From 8ca2be7f9a0ab5eb86415897247a91426c2d556c Mon Sep 17 00:00:00 2001 From: Bjay kamwa Watanabe Date: Wed, 10 Jun 2026 17:55:22 +0200 Subject: [PATCH 1/3] Refresh OpenAPI spec with task and social publishing endpoints. Sync from tellers-backend openapi.filtered.yaml so the generated client includes user task polling/cancel, social publishing, and updated export docs. Co-authored-by: Cursor --- .../openapi.tellers_public_api.yaml | 441 +++++++++++++++++- 1 file changed, 434 insertions(+), 7 deletions(-) diff --git a/src/tellers_api/openapi.tellers_public_api.yaml b/src/tellers_api/openapi.tellers_public_api.yaml index e243a38..de3bff3 100644 --- a/src/tellers_api/openapi.tellers_public_api.yaml +++ b/src/tellers_api/openapi.tellers_public_api.yaml @@ -415,6 +415,93 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /users/tasks/{task_id}: + get: + tags: + - accepts-api-key + summary: Get User Task + operationId: get_user_task_users_tasks__task_id__get + parameters: + - name: task_id + in: path + required: true + schema: + type: string + title: Task Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/PendingTaskResponse' + - $ref: '#/components/schemas/CompleteTaskResponse' + - $ref: '#/components/schemas/FailedTaskResponse' + title: Response Get User Task Users Tasks Task Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - accepts-api-key + summary: Cancel User Task + operationId: cancel_user_task_users_tasks__task_id__delete + parameters: + - name: task_id + in: path + required: true + schema: + type: string + title: Task Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/CancelTaskResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /users/tasks: get: tags: @@ -898,17 +985,18 @@ paths: description: 'Start async export of project to MP4 at one or more resolutions. Creates the export asset - in the API, then enqueues a task that generates each rendition. The export - appears as a + in the API, then enqueues a task that browser-encodes the highest requested + resolution. - new video asset in the project''s export folder (project name + date + .mp4), - with multiple + Smaller renditions are enqueued as downscaling_task jobs (not chained). The + export - renditions (e.g. 480p, 720p, 1080p, 4k) attached to that asset. + appears as a new video asset in the project''s export folder (project name + + date + .mp4). - Returns task_id and asset_id. Poll GET /task/{task_id} for completion; result - includes asset_id.' + Returns task_id and asset_id. Poll GET /users/tasks/{task_id} for completion; + result includes asset_id.' operationId: export_project_project__project_id__export_post parameters: - name: project_id @@ -961,6 +1049,98 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /social-publishing/connect: + get: + tags: + - accepts-api-key + summary: Redirect To Upload Post Connect + operationId: redirect_to_upload_post_connect_social_publishing_connect_get + parameters: + - name: platforms + in: query + required: false + schema: + type: array + items: + type: string + title: Platforms + - name: redirect_url + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Redirect Url + - name: show_calendar + in: query + required: false + schema: + type: boolean + default: false + title: Show Calendar + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /social-publishing/posts: + post: + tags: + - accepts-api-key + summary: Publish Social Post + operationId: publish_social_post_social_publishing_posts_post + parameters: + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SocialPublishPostRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/SocialPublishPostResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /billing/catalog: get: tags: @@ -1626,6 +1806,95 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /task/{task_id}: + get: + tags: + - accepts-api-key + summary: Get Task + operationId: get_task_task__task_id__get + deprecated: true + parameters: + - name: task_id + in: path + required: true + schema: + type: string + title: Task Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/PendingTaskResponse' + - $ref: '#/components/schemas/CompleteTaskResponse' + - $ref: '#/components/schemas/FailedTaskResponse' + title: Response Get Task Task Task Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - accepts-api-key + summary: Delete Task + operationId: delete_task_task__task_id__delete + deprecated: true + parameters: + - name: task_id + in: path + required: true + schema: + type: string + title: Task Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/CancelTaskResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' components: schemas: AgentMessageRequest: @@ -2085,6 +2354,11 @@ components: maintenance_mode: type: boolean title: Maintenance Mode + social_publishing_platforms: + items: + $ref: '#/components/schemas/SocialPublishingPlatformSetting' + type: array + title: Social Publishing Platforms available_agent_tools: items: additionalProperties: true @@ -2104,6 +2378,7 @@ components: type: object required: - maintenance_mode + - social_publishing_platforms - available_agent_tools - available_llm_models - presets @@ -2478,6 +2753,22 @@ components: required: - plans title: BillingCatalog + CancelTaskResponse: + properties: + task_id: + type: string + title: Task Id + previous_state: + $ref: '#/components/schemas/TaskStatus' + description: + type: string + title: Description + type: object + required: + - task_id + - previous_state + - description + title: CancelTaskResponse CatalogFeature: properties: text: @@ -2819,6 +3110,23 @@ components: - context - created_at title: ChatMessageUserResponse + CompleteTaskResponse: + properties: + status: + type: string + const: COMPLETE + title: Status + default: COMPLETE + result: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + title: Result + type: object + required: + - result + title: CompleteTaskResponse CouponCheckResponse: properties: coupon_code: @@ -2903,6 +3211,21 @@ components: title: ExportProjectResponse description: 'Response for POST /project/{project_id}/export: task_id and the new export asset_id.' + FailedTaskResponse: + properties: + status: + type: string + const: FAILED + title: Status + default: FAILED + result: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + title: Result + type: object + title: FailedTaskResponse FileReference: properties: file_name: @@ -3007,6 +3330,26 @@ components: - multipart_upload_id - parts title: MultipartCompleteRequest + PendingTaskResponse: + properties: + status: + type: string + const: PENDING + title: Status + default: PENDING + progress: + anyOf: + - type: number + - type: 'null' + title: Progress + result: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + title: Result + type: object + title: PendingTaskResponse PlanLineItem: properties: price_id: @@ -3102,6 +3445,78 @@ components: required: - task_id title: RequestTaskResponse + SocialPublishPostRequest: + properties: + asset_id: + type: string + title: Asset Id + platforms: + items: + type: string + type: array + minItems: 1 + title: Platforms + title: + type: string + title: Title + default: '' + description: + type: string + title: Description + default: '' + scheduled_date: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Scheduled Date + type: object + required: + - asset_id + - platforms + title: SocialPublishPostRequest + SocialPublishPostResponse: + properties: + status: + type: string + title: Status + request_id: + anyOf: + - type: string + - type: 'null' + title: Request Id + job_id: + anyOf: + - type: string + - type: 'null' + title: Job Id + provider_response: + additionalProperties: true + type: object + title: Provider Response + type: object + required: + - status + title: SocialPublishPostResponse + SocialPublishingPlatformSetting: + properties: + id: + type: string + title: Id + label: + type: string + title: Label + supported_media_types: + items: + type: string + type: array + title: Supported Media Types + type: object + required: + - id + - label + - supported_media_types + title: SocialPublishingPlatformSetting SourceFileInfo: properties: sourceName: @@ -3262,6 +3677,18 @@ components: - ended_at - error_message title: TaskResponse + TaskStatus: + type: string + enum: + - FAILURE + - PENDING + - RECEIVED + - RETRY + - REVOKED + - STARTED + - SUCCESS + - PROGRESS + title: TaskStatus TellersAvailableOptions: properties: verbosity: From 194039e098983fa05465edaa3292b18ce1677bbb Mon Sep 17 00:00:00 2001 From: Bjay kamwa Watanabe Date: Wed, 10 Jun 2026 18:14:23 +0200 Subject: [PATCH 2/3] Wire CLI commands to new task and social publishing APIs. Add task get/wait/cancel via GET/DELETE /users/tasks/{task_id}, social connect/publish commands, and --wait on project export to poll until completion. Co-authored-by: Cursor --- src/cli.rs | 4 + src/commands/api_config.rs | 19 +++ src/commands/mod.rs | 2 + src/commands/project/export.rs | 49 +++--- src/commands/social.rs | 181 ++++++++++++++++++++++ src/commands/task.rs | 264 +++++++++++++++++++++++++++++++++ src/main.rs | 12 ++ 7 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 src/commands/social.rs create mode 100644 src/commands/task.rs diff --git a/src/cli.rs b/src/cli.rs index c46f589..c16713a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,6 +2,8 @@ use clap::{Parser, Subcommand}; use crate::commands::asset::AssetArgs; use crate::commands::project::ProjectArgs; +use crate::commands::social::SocialArgs; +use crate::commands::task::TaskArgs; use crate::commands::upload::UploadArgs; #[derive(Parser, Debug)] @@ -44,6 +46,8 @@ pub struct Cli { pub enum Command { Asset(AssetArgs), Project(ProjectArgs), + Social(SocialArgs), + Task(TaskArgs), Upload(UploadArgs), } diff --git a/src/commands/api_config.rs b/src/commands/api_config.rs index efbb198..18e61e7 100644 --- a/src/commands/api_config.rs +++ b/src/commands/api_config.rs @@ -31,3 +31,22 @@ pub fn create_config() -> Configuration { cfg } +pub fn format_api_error(e: &tellers_api_client::apis::Error) -> String { + let mut message = format!("{}", e); + match e { + tellers_api_client::apis::Error::Reqwest(req_err) => { + if let Some(status) = req_err.status() { + message.push_str(&format!("; http_status: {}", status)); + } + } + tellers_api_client::apis::Error::ResponseError(resp) => { + message.push_str(&format!("; http_status: {}", resp.status)); + if !resp.content.is_empty() { + message.push_str(&format!("; response: {}", resp.content)); + } + } + _ => {} + } + message +} + diff --git a/src/commands/mod.rs b/src/commands/mod.rs index aa83642..a15ab12 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,4 +2,6 @@ pub mod api_config; pub mod asset; pub mod prompt; pub mod project; +pub mod social; +pub mod task; pub mod upload; diff --git a/src/commands/project/export.rs b/src/commands/project/export.rs index ef3ae26..c384527 100644 --- a/src/commands/project/export.rs +++ b/src/commands/project/export.rs @@ -1,7 +1,10 @@ +use std::time::Duration; + use clap::Args; use tellers_api_client::apis::accepts_api_key_api as api; -use crate::commands::api_config; +use crate::commands::{api_config, task}; +use crate::output; #[derive(Args, Debug)] pub struct ExportArgs { @@ -18,6 +21,10 @@ pub struct ExportArgs { #[arg(long, env = "TELLERS_AUTH_BEARER")] pub auth_bearer: Option, + + /// Poll GET /users/tasks/{task_id} until the export completes. + #[arg(long, default_value_t = false)] + pub wait: bool, } const ALLOWED_RENDITIONS: &[&str] = &["360p", "480p", "720p", "1080p", "1440p", "4k"]; @@ -66,27 +73,31 @@ pub fn run(args: ExportArgs) -> Result<(), String> { bearer_header.as_deref(), ) .await - .map_err(|e| { - let mut m = format!("export failed: {}", e); - match &e { - tellers_api_client::apis::Error::Reqwest(req_err) => { - if let Some(status) = req_err.status() { - m.push_str(&format!("; http_status: {}", status)); - } - } - tellers_api_client::apis::Error::ResponseError(resp) => { - m.push_str(&format!("; http_status: {}", resp.status)); - if !resp.content.is_empty() { - m.push_str(&format!("; response: {}", resp.content)); - } - } - _ => {} - } - m - })?; + .map_err(|e| api_config::format_api_error(&e))?; println!("task_id: {}", resp.task_id); println!("asset_id: {}", resp.asset_id); + + if args.wait { + output::info(format!( + "Waiting for export task {} to complete...", + resp.task_id + )); + let result = task::wait_for_user_task( + &cfg, + &resp.task_id, + &api_key, + bearer_header.as_deref(), + Duration::from_secs(2), + ) + .await?; + println!( + "{}", + serde_json::to_string_pretty(&result) + .map_err(|e| format!("failed to encode export result: {}", e))? + ); + } + Ok(()) }) } diff --git a/src/commands/social.rs b/src/commands/social.rs new file mode 100644 index 0000000..05844e6 --- /dev/null +++ b/src/commands/social.rs @@ -0,0 +1,181 @@ +use clap::{Args, Subcommand}; +use tellers_api_client::apis::accepts_api_key_api as api; +use tellers_api_client::models::SocialPublishPostRequest; + +use crate::commands::api_config; + +#[derive(Args, Debug)] +pub struct SocialArgs { + #[command(subcommand)] + pub command: SocialCommand, +} + +#[derive(Subcommand, Debug)] +pub enum SocialCommand { + /// Get the Upload-Post OAuth connect URL for linking social accounts. + Connect(ConnectArgs), + /// Publish an asset to one or more social platforms. + Publish(PublishArgs), +} + +#[derive(Args, Debug)] +pub struct ConnectArgs { + /// Platform(s) to connect (repeat for multiple). + #[arg(long = "platform", value_name = "PLATFORM")] + pub platforms: Vec, + + #[arg(long)] + pub redirect_url: Option, + + #[arg(long, default_value_t = false)] + pub show_calendar: bool, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +#[derive(Args, Debug)] +pub struct PublishArgs { + /// Asset ID to publish. + pub asset_id: String, + + /// Target platform(s) (repeat for multiple). + #[arg(long = "platform", required = true, value_name = "PLATFORM")] + pub platforms: Vec, + + #[arg(long, default_value = "")] + pub title: String, + + #[arg(long, default_value = "")] + pub description: String, + + /// ISO 8601 datetime for scheduled publishing. + #[arg(long)] + pub scheduled_date: Option, + + #[arg(long, env = "TELLERS_API_KEY")] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +pub fn run(args: SocialArgs) -> Result<(), String> { + match args.command { + SocialCommand::Connect(connect_args) => run_connect(connect_args), + SocialCommand::Publish(publish_args) => run_publish(publish_args), + } +} + +fn run_connect(args: ConnectArgs) -> Result<(), String> { + let bearer = api_config::get_bearer_header(args.auth_bearer) + .ok_or_else(|| "TELLERS_AUTH_BEARER required for social connect".to_string())?; + let base = api_config::get_api_base(); + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| format!("failed to build HTTP client: {}", e))?; + + let mut url = reqwest::Url::parse(&format!( + "{}/social-publishing/connect", + base.trim_end_matches('/') + )) + .map_err(|e| format!("invalid API base URL: {}", e))?; + + { + let mut pairs = url.query_pairs_mut(); + for platform in &args.platforms { + pairs.append_pair("platforms", platform); + } + if let Some(redirect_url) = &args.redirect_url { + pairs.append_pair("redirect_url", redirect_url); + } + if args.show_calendar { + pairs.append_pair("show_calendar", "true"); + } + } + + let resp = client + .get(url) + .header("authorization", &bearer) + .send() + .await + .map_err(|e| format!("connect request failed: {}", e))?; + + if resp.status().is_redirection() { + let location = resp + .headers() + .get(reqwest::header::LOCATION) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + format!( + "connect returned redirect without Location header (http_status: {})", + resp.status() + ) + })?; + println!("{}", location); + return Ok(()); + } + + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + if status.is_success() { + if body.is_empty() { + return Err("connect succeeded but returned no redirect URL".to_string()); + } + println!("{}", body); + return Ok(()); + } + + Err(format!( + "connect failed; http_status: {}; response: {}", + status, body + )) + }) +} + +fn run_publish(args: PublishArgs) -> Result<(), String> { + let cfg = api_config::create_config(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer = api_config::get_bearer_header(args.auth_bearer); + + let mut body = SocialPublishPostRequest::new(args.asset_id, args.platforms); + body.title = Some(args.title); + body.description = Some(args.description); + if let Some(scheduled_date) = args.scheduled_date { + body.scheduled_date = Some(Some(scheduled_date)); + } + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let resp = api::publish_social_post_social_publishing_posts_post( + &cfg, + body, + Some(&api_key), + bearer.as_deref(), + ) + .await + .map_err(|e| api_config::format_api_error(&e))?; + + println!("status: {}", resp.status); + if let Some(request_id) = resp.request_id.and_then(|v| v) { + println!("request_id: {}", request_id); + } + if let Some(job_id) = resp.job_id.and_then(|v| v) { + println!("job_id: {}", job_id); + } + if let Some(provider_response) = resp.provider_response { + println!( + "provider_response: {}", + serde_json::to_string_pretty(&provider_response) + .unwrap_or_else(|_| format!("{:?}", provider_response)) + ); + } + Ok(()) + }) +} diff --git a/src/commands/task.rs b/src/commands/task.rs new file mode 100644 index 0000000..b672a59 --- /dev/null +++ b/src/commands/task.rs @@ -0,0 +1,264 @@ +use std::time::Duration; + +use clap::{Args, Subcommand}; +use serde::Deserialize; +use tellers_api_client::apis::accepts_api_key_api as api; +use tellers_api_client::apis::configuration::Configuration; +use tokio::time::sleep; + +use crate::commands::api_config; +use crate::output; + +#[derive(Args, Debug)] +pub struct TaskArgs { + #[command(subcommand)] + pub command: TaskCommand, +} + +#[derive(Subcommand, Debug)] +pub enum TaskCommand { + /// Fetch the current status of a user task. + Get(GetArgs), + /// Poll a user task until it completes or fails. + Wait(WaitArgs), + /// Cancel a running user task. + Cancel(CancelArgs), +} + +#[derive(Args, Debug)] +pub struct GetArgs { + pub task_id: String, + + #[arg(long, env = "TELLERS_API_KEY")] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +#[derive(Args, Debug)] +pub struct WaitArgs { + pub task_id: String, + + #[arg(long, default_value_t = 2)] + pub interval_secs: u64, + + #[arg(long, env = "TELLERS_API_KEY")] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +#[derive(Args, Debug)] +pub struct CancelArgs { + pub task_id: String, + + #[arg(long, env = "TELLERS_API_KEY")] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +#[derive(Debug, Clone)] +pub enum UserTaskStatus { + Pending { progress: Option }, + Complete { result: serde_json::Value }, + Failed { result: Option }, +} + +#[derive(Deserialize)] +struct RawUserTaskResponse { + status: String, + #[serde(default)] + progress: Option, + #[serde(default)] + result: Option, +} + +pub fn run(args: TaskArgs) -> Result<(), String> { + match args.command { + TaskCommand::Get(get_args) => run_get(get_args), + TaskCommand::Wait(wait_args) => run_wait(wait_args), + TaskCommand::Cancel(cancel_args) => run_cancel(cancel_args), + } +} + +fn run_get(args: GetArgs) -> Result<(), String> { + let cfg = api_config::create_config(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer = api_config::get_bearer_header(args.auth_bearer); + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let status = + fetch_user_task(&cfg, &args.task_id, &api_key, bearer.as_deref()).await?; + println!("{}", serde_json::to_string_pretty(&status_to_json(status)) + .map_err(|e| format!("failed to encode task status: {}", e))?); + Ok(()) + }) +} + +fn run_wait(args: WaitArgs) -> Result<(), String> { + let cfg = api_config::create_config(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer = api_config::get_bearer_header(args.auth_bearer); + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + output::info(format!( + "Polling /users/tasks/{} every {}s...", + args.task_id, args.interval_secs + )); + let result = wait_for_user_task( + &cfg, + &args.task_id, + &api_key, + bearer.as_deref(), + Duration::from_secs(args.interval_secs), + ) + .await?; + println!("{}", serde_json::to_string_pretty(&result) + .map_err(|e| format!("failed to encode task result: {}", e))?); + Ok(()) + }) +} + +fn run_cancel(args: CancelArgs) -> Result<(), String> { + let cfg = api_config::create_config(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer = api_config::get_bearer_header(args.auth_bearer); + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let resp = api::cancel_user_task_users_tasks_task_id_delete( + &cfg, + &args.task_id, + Some(&api_key), + bearer.as_deref(), + ) + .await + .map_err(|e| api_config::format_api_error(&e))?; + + println!("task_id: {}", resp.task_id); + println!("previous_state: {:?}", resp.previous_state); + println!("description: {}", resp.description); + Ok(()) + }) +} + +pub async fn fetch_user_task( + cfg: &Configuration, + task_id: &str, + api_key: &str, + bearer: Option<&str>, +) -> Result { + let uri = format!( + "{}/users/tasks/{}", + cfg.base_path.trim_end_matches('/'), + urlencoding_encode(task_id) + ); + let mut req = cfg.client.request(reqwest::Method::GET, &uri); + if let Some(user_agent) = &cfg.user_agent { + req = req.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + req = req.header("x-api-key", api_key); + if let Some(bearer) = bearer { + req = req.header("authorization", bearer); + } + + let resp = req + .send() + .await + .map_err(|e| format!("failed to fetch task: {}", e))?; + let status_code = resp.status(); + let body = resp + .text() + .await + .map_err(|e| format!("failed to read task response: {}", e))?; + if !status_code.is_success() { + return Err(format!( + "failed to fetch task; http_status: {}; response: {}", + status_code, body + )); + } + + let raw: RawUserTaskResponse = serde_json::from_str(&body) + .map_err(|e| format!("failed to parse task response: {}; body: {}", e, body))?; + + parse_user_task_status(&raw) +} + +pub async fn wait_for_user_task( + cfg: &Configuration, + task_id: &str, + api_key: &str, + bearer: Option<&str>, + poll_interval: Duration, +) -> Result { + loop { + match fetch_user_task(cfg, task_id, api_key, bearer).await? { + UserTaskStatus::Pending { progress } => { + if let Some(progress) = progress { + output::info(format!("task progress: {:.0}%", progress_to_percent(progress))); + } + sleep(poll_interval).await; + } + UserTaskStatus::Complete { result } => return Ok(result), + UserTaskStatus::Failed { result } => { + let detail = result + .map(|v| v.to_string()) + .unwrap_or_else(|| "unknown error".to_string()); + return Err(format!("task failed: {}", detail)); + } + } + } +} + +fn parse_user_task_status(raw: &RawUserTaskResponse) -> Result { + match raw.status.as_str() { + "PENDING" => Ok(UserTaskStatus::Pending { + progress: raw.progress, + }), + "COMPLETE" => Ok(UserTaskStatus::Complete { + result: raw.result.clone().unwrap_or(serde_json::Value::Null), + }), + "FAILED" => Ok(UserTaskStatus::Failed { + result: raw.result.clone(), + }), + other => Err(format!("unknown task status: {}", other)), + } +} + +fn status_to_json(status: UserTaskStatus) -> serde_json::Value { + match status { + UserTaskStatus::Pending { progress } => serde_json::json!({ + "status": "PENDING", + "progress": progress, + }), + UserTaskStatus::Complete { result } => serde_json::json!({ + "status": "COMPLETE", + "result": result, + }), + UserTaskStatus::Failed { result } => serde_json::json!({ + "status": "FAILED", + "result": result, + }), + } +} + +fn progress_to_percent(progress: f64) -> f64 { + if (0.0..=1.0).contains(&progress) { + progress * 100.0 + } else { + progress.clamp(0.0, 100.0) + } +} + +fn urlencoding_encode(value: &str) -> String { + url::form_urlencoded::byte_serialize(value.as_bytes()).collect() +} diff --git a/src/main.rs b/src/main.rs index b82ae66..aaa4836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,18 @@ fn main() { } } } + Some(cli::Command::Social(social_args)) => { + if let Err(error) = commands::social::run(social_args) { + eprintln!("error: {}", error); + std::process::exit(1); + } + } + Some(cli::Command::Task(task_args)) => { + if let Err(error) = commands::task::run(task_args) { + eprintln!("error: {}", error); + std::process::exit(1); + } + } Some(cli::Command::Upload(upload_args)) => { let suppress_plain_error = matches!( &upload_args.command, From 399788eeb5854758ffd7c06b5bbecffebe500210 Mon Sep 17 00:00:00 2001 From: Bjay kamwa Watanabe Date: Wed, 10 Jun 2026 18:15:26 +0200 Subject: [PATCH 3/3] Sync OpenAPI spec and remove social publishing CLI commands. Refresh from backend openapi.filtered.yaml after social endpoints were dropped; keep task polling APIs only. Co-authored-by: Cursor --- src/cli.rs | 2 - src/commands/mod.rs | 1 - src/commands/social.rs | 181 ------------------ src/main.rs | 6 - .../openapi.tellers_public_api.yaml | 145 -------------- 5 files changed, 335 deletions(-) delete mode 100644 src/commands/social.rs diff --git a/src/cli.rs b/src/cli.rs index c16713a..1c37342 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,6 @@ use clap::{Parser, Subcommand}; use crate::commands::asset::AssetArgs; use crate::commands::project::ProjectArgs; -use crate::commands::social::SocialArgs; use crate::commands::task::TaskArgs; use crate::commands::upload::UploadArgs; @@ -46,7 +45,6 @@ pub struct Cli { pub enum Command { Asset(AssetArgs), Project(ProjectArgs), - Social(SocialArgs), Task(TaskArgs), Upload(UploadArgs), } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a15ab12..d718de7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,6 +2,5 @@ pub mod api_config; pub mod asset; pub mod prompt; pub mod project; -pub mod social; pub mod task; pub mod upload; diff --git a/src/commands/social.rs b/src/commands/social.rs deleted file mode 100644 index 05844e6..0000000 --- a/src/commands/social.rs +++ /dev/null @@ -1,181 +0,0 @@ -use clap::{Args, Subcommand}; -use tellers_api_client::apis::accepts_api_key_api as api; -use tellers_api_client::models::SocialPublishPostRequest; - -use crate::commands::api_config; - -#[derive(Args, Debug)] -pub struct SocialArgs { - #[command(subcommand)] - pub command: SocialCommand, -} - -#[derive(Subcommand, Debug)] -pub enum SocialCommand { - /// Get the Upload-Post OAuth connect URL for linking social accounts. - Connect(ConnectArgs), - /// Publish an asset to one or more social platforms. - Publish(PublishArgs), -} - -#[derive(Args, Debug)] -pub struct ConnectArgs { - /// Platform(s) to connect (repeat for multiple). - #[arg(long = "platform", value_name = "PLATFORM")] - pub platforms: Vec, - - #[arg(long)] - pub redirect_url: Option, - - #[arg(long, default_value_t = false)] - pub show_calendar: bool, - - #[arg(long, env = "TELLERS_AUTH_BEARER")] - pub auth_bearer: Option, -} - -#[derive(Args, Debug)] -pub struct PublishArgs { - /// Asset ID to publish. - pub asset_id: String, - - /// Target platform(s) (repeat for multiple). - #[arg(long = "platform", required = true, value_name = "PLATFORM")] - pub platforms: Vec, - - #[arg(long, default_value = "")] - pub title: String, - - #[arg(long, default_value = "")] - pub description: String, - - /// ISO 8601 datetime for scheduled publishing. - #[arg(long)] - pub scheduled_date: Option, - - #[arg(long, env = "TELLERS_API_KEY")] - pub api_key: Option, - - #[arg(long, env = "TELLERS_AUTH_BEARER")] - pub auth_bearer: Option, -} - -pub fn run(args: SocialArgs) -> Result<(), String> { - match args.command { - SocialCommand::Connect(connect_args) => run_connect(connect_args), - SocialCommand::Publish(publish_args) => run_publish(publish_args), - } -} - -fn run_connect(args: ConnectArgs) -> Result<(), String> { - let bearer = api_config::get_bearer_header(args.auth_bearer) - .ok_or_else(|| "TELLERS_AUTH_BEARER required for social connect".to_string())?; - let base = api_config::get_api_base(); - - tokio::runtime::Runtime::new() - .map_err(|e| format!("failed to start runtime: {}", e))? - .block_on(async move { - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .map_err(|e| format!("failed to build HTTP client: {}", e))?; - - let mut url = reqwest::Url::parse(&format!( - "{}/social-publishing/connect", - base.trim_end_matches('/') - )) - .map_err(|e| format!("invalid API base URL: {}", e))?; - - { - let mut pairs = url.query_pairs_mut(); - for platform in &args.platforms { - pairs.append_pair("platforms", platform); - } - if let Some(redirect_url) = &args.redirect_url { - pairs.append_pair("redirect_url", redirect_url); - } - if args.show_calendar { - pairs.append_pair("show_calendar", "true"); - } - } - - let resp = client - .get(url) - .header("authorization", &bearer) - .send() - .await - .map_err(|e| format!("connect request failed: {}", e))?; - - if resp.status().is_redirection() { - let location = resp - .headers() - .get(reqwest::header::LOCATION) - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - format!( - "connect returned redirect without Location header (http_status: {})", - resp.status() - ) - })?; - println!("{}", location); - return Ok(()); - } - - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - if status.is_success() { - if body.is_empty() { - return Err("connect succeeded but returned no redirect URL".to_string()); - } - println!("{}", body); - return Ok(()); - } - - Err(format!( - "connect failed; http_status: {}; response: {}", - status, body - )) - }) -} - -fn run_publish(args: PublishArgs) -> Result<(), String> { - let cfg = api_config::create_config(); - let api_key = api_config::get_api_key(args.api_key)?; - let bearer = api_config::get_bearer_header(args.auth_bearer); - - let mut body = SocialPublishPostRequest::new(args.asset_id, args.platforms); - body.title = Some(args.title); - body.description = Some(args.description); - if let Some(scheduled_date) = args.scheduled_date { - body.scheduled_date = Some(Some(scheduled_date)); - } - - tokio::runtime::Runtime::new() - .map_err(|e| format!("failed to start runtime: {}", e))? - .block_on(async move { - let resp = api::publish_social_post_social_publishing_posts_post( - &cfg, - body, - Some(&api_key), - bearer.as_deref(), - ) - .await - .map_err(|e| api_config::format_api_error(&e))?; - - println!("status: {}", resp.status); - if let Some(request_id) = resp.request_id.and_then(|v| v) { - println!("request_id: {}", request_id); - } - if let Some(job_id) = resp.job_id.and_then(|v| v) { - println!("job_id: {}", job_id); - } - if let Some(provider_response) = resp.provider_response { - println!( - "provider_response: {}", - serde_json::to_string_pretty(&provider_response) - .unwrap_or_else(|_| format!("{:?}", provider_response)) - ); - } - Ok(()) - }) -} diff --git a/src/main.rs b/src/main.rs index aaa4836..eb2f93c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,12 +43,6 @@ fn main() { } } } - Some(cli::Command::Social(social_args)) => { - if let Err(error) = commands::social::run(social_args) { - eprintln!("error: {}", error); - std::process::exit(1); - } - } Some(cli::Command::Task(task_args)) => { if let Err(error) = commands::task::run(task_args) { eprintln!("error: {}", error); diff --git a/src/tellers_api/openapi.tellers_public_api.yaml b/src/tellers_api/openapi.tellers_public_api.yaml index de3bff3..932c773 100644 --- a/src/tellers_api/openapi.tellers_public_api.yaml +++ b/src/tellers_api/openapi.tellers_public_api.yaml @@ -1049,98 +1049,6 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /social-publishing/connect: - get: - tags: - - accepts-api-key - summary: Redirect To Upload Post Connect - operationId: redirect_to_upload_post_connect_social_publishing_connect_get - parameters: - - name: platforms - in: query - required: false - schema: - type: array - items: - type: string - title: Platforms - - name: redirect_url - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Redirect Url - - name: show_calendar - in: query - required: false - schema: - type: boolean - default: false - title: Show Calendar - - name: authorization - in: header - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Authorization - responses: - '200': - description: Successful Response - content: - application/json: - schema: {} - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /social-publishing/posts: - post: - tags: - - accepts-api-key - summary: Publish Social Post - operationId: publish_social_post_social_publishing_posts_post - parameters: - - name: x-api-key - in: header - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: X-Api-Key - - name: authorization - in: header - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Authorization - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SocialPublishPostRequest' - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/SocialPublishPostResponse' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' /billing/catalog: get: tags: @@ -3445,59 +3353,6 @@ components: required: - task_id title: RequestTaskResponse - SocialPublishPostRequest: - properties: - asset_id: - type: string - title: Asset Id - platforms: - items: - type: string - type: array - minItems: 1 - title: Platforms - title: - type: string - title: Title - default: '' - description: - type: string - title: Description - default: '' - scheduled_date: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Scheduled Date - type: object - required: - - asset_id - - platforms - title: SocialPublishPostRequest - SocialPublishPostResponse: - properties: - status: - type: string - title: Status - request_id: - anyOf: - - type: string - - type: 'null' - title: Request Id - job_id: - anyOf: - - type: string - - type: 'null' - title: Job Id - provider_response: - additionalProperties: true - type: object - title: Provider Response - type: object - required: - - status - title: SocialPublishPostResponse SocialPublishingPlatformSetting: properties: id: