mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat: Add Google Suite & Telegram WASM tools (#9)
* Add Google Calendar and Gmail WASM tools, and /add-tool skill Scaffold two new WASM tools that share a single Google OAuth token: - google-calendar: list/get/create/update/delete calendar events - gmail: list/search/get/send/draft/reply/trash emails Both tools use the sandboxed WIT interface with strict HTTP allowlists, credential injection, and rate limiting. OAuth config requests only the minimum scopes needed (calendar.events, gmail.modify, gmail.compose). Also adds the /add-tool skill for scaffolding future WASM or built-in tools with all boilerplate wired up. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Document WASM vs MCP server decision guide in CLAUDE.md Co-Authored-By: Claude Opus 4.6 <[email protected]> * Add Google Drive WASM tool with full file and sharing management Supports 12 actions: list/get/download/upload/update files, create folders, delete/trash, share/list/remove permissions, and list shared drives. Works with both personal and organizational drives via the corpora parameter. Uses shared google_oauth_token for auth. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Add Google Sheets, Docs, and Slides WASM tools Three new Google Workspace tools sharing google_oauth_token: - Sheets: create spreadsheets, read/write/append values, manage sheets, format cells - Docs: create/read/edit documents, text formatting, paragraphs, tables, lists - Slides: create/edit presentations, shapes, images, text formatting, thumbnails, templates Also adds tools-src/TOOLS.md tracking implementation status. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Add Telegram WASM tool with direct MTProto over HTTPS Replace TDLight Docker dependency with pure-Rust grammers crates for direct encrypted MTProto communication to Telegram's web transport endpoints. No middleware, no Docker needed. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Gitignore Cargo.lock files in WASM tools Library crates should not commit lock files. Consolidate per-tool .gitignore into a single one at wasm-tools/ level. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Flatten tools-src/wasm-tools/ into tools-src/ All tools are WASM, the extra nesting added no value. Moves all tool crates up one level, updates WIT paths and documentation references. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Fix Slack tool: add OAuth auth, URL encoding, pin wit-bindgen - Add OAuth 2.0 auth section to Slack capabilities with proper scopes and manual fallback instructions - URL-encode query parameters in GET requests to prevent injection - Remove dead SlackApiError struct - Pin wit-bindgen to =0.36 across all WASM tools for Rust 1.86 compat - Update add-tool template with pinned version Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e6725eb6d9
commit
a35db4d32d
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "google-sheets-tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Google Sheets integration tool for IronClaw (WASM component)"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "=0.36"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{
|
||||
"host": "sheets.googleapis.com",
|
||||
"path_prefix": "/v4/spreadsheets",
|
||||
"methods": ["GET", "POST", "PUT"]
|
||||
}
|
||||
],
|
||||
"credentials": {
|
||||
"google_oauth_token": {
|
||||
"secret_name": "google_oauth_token",
|
||||
"location": { "type": "bearer" },
|
||||
"host_patterns": ["sheets.googleapis.com"]
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"requests_per_minute": 60,
|
||||
"requests_per_hour": 500
|
||||
},
|
||||
"timeout_secs": 30
|
||||
},
|
||||
"secrets": {
|
||||
"allowed_names": ["google_oauth_token"]
|
||||
},
|
||||
"auth": {
|
||||
"secret_name": "google_oauth_token",
|
||||
"display_name": "Google",
|
||||
"oauth": {
|
||||
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
|
||||
"client_secret_env": "GOOGLE_OAUTH_CLIENT_SECRET",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/spreadsheets"
|
||||
],
|
||||
"use_pkce": false,
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"env_var": "GOOGLE_OAUTH_TOKEN"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
//! Google Sheets API v4 implementation.
|
||||
//!
|
||||
//! All API calls go through the host's HTTP capability, which handles
|
||||
//! credential injection and rate limiting. The WASM tool never sees
|
||||
//! the actual OAuth token.
|
||||
|
||||
use crate::near::agent::host;
|
||||
use crate::types::*;
|
||||
|
||||
const SHEETS_API_BASE: &str = "https://sheets.googleapis.com/v4/spreadsheets";
|
||||
|
||||
/// Make a Google Sheets API call.
|
||||
fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, String> {
|
||||
let url = if path.is_empty() {
|
||||
SHEETS_API_BASE.to_string()
|
||||
} else {
|
||||
format!("{}/{}", SHEETS_API_BASE, path)
|
||||
};
|
||||
|
||||
let headers = if body.is_some() {
|
||||
r#"{"Content-Type": "application/json"}"#
|
||||
} else {
|
||||
"{}"
|
||||
};
|
||||
|
||||
let body_bytes = body.map(|b| b.as_bytes().to_vec());
|
||||
|
||||
host::log(
|
||||
host::LogLevel::Debug,
|
||||
&format!("Google Sheets API: {} {}", method, url),
|
||||
);
|
||||
|
||||
let response = host::http_request(method, &url, headers, body_bytes.as_deref())?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body_text = String::from_utf8_lossy(&response.body);
|
||||
return Err(format!(
|
||||
"Google Sheets API returned status {}: {}",
|
||||
response.status, body_text
|
||||
));
|
||||
}
|
||||
|
||||
if response.body.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8 in response: {}", e))
|
||||
}
|
||||
|
||||
/// Parse sheet info from the API's JSON.
|
||||
fn parse_sheet_info(v: &serde_json::Value) -> SheetInfo {
|
||||
let props = &v["properties"];
|
||||
let grid = &props["gridProperties"];
|
||||
SheetInfo {
|
||||
sheet_id: props["sheetId"].as_i64().unwrap_or(0),
|
||||
title: props["title"].as_str().unwrap_or("").to_string(),
|
||||
index: props["index"].as_i64().unwrap_or(0),
|
||||
row_count: grid["rowCount"].as_i64().unwrap_or(0),
|
||||
column_count: grid["columnCount"].as_i64().unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a named range from the API's JSON.
|
||||
fn parse_named_range(v: &serde_json::Value) -> NamedRange {
|
||||
let range = &v["range"];
|
||||
let range_str = format_grid_range(range);
|
||||
NamedRange {
|
||||
named_range_id: v["namedRangeId"].as_str().unwrap_or("").to_string(),
|
||||
name: v["name"].as_str().unwrap_or("").to_string(),
|
||||
range: range_str,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a GridRange into a human-readable string.
|
||||
fn format_grid_range(v: &serde_json::Value) -> String {
|
||||
let sheet_id = v["sheetId"].as_i64().unwrap_or(0);
|
||||
let start_row = v["startRowIndex"].as_i64().unwrap_or(0);
|
||||
let end_row = v["endRowIndex"].as_i64().unwrap_or(0);
|
||||
let start_col = v["startColumnIndex"].as_i64().unwrap_or(0);
|
||||
let end_col = v["endColumnIndex"].as_i64().unwrap_or(0);
|
||||
format!(
|
||||
"sheetId={}, rows {}:{}, cols {}:{}",
|
||||
sheet_id, start_row, end_row, start_col, end_col
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new spreadsheet.
|
||||
pub fn create_spreadsheet(
|
||||
title: &str,
|
||||
sheet_names: &[String],
|
||||
) -> Result<CreateSpreadsheetResult, String> {
|
||||
let sheets: Vec<serde_json::Value> = if sheet_names.is_empty() {
|
||||
vec![serde_json::json!({"properties": {"title": "Sheet1"}})]
|
||||
} else {
|
||||
sheet_names
|
||||
.iter()
|
||||
.map(|name| serde_json::json!({"properties": {"title": name}}))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let body = serde_json::json!({
|
||||
"properties": {"title": title},
|
||||
"sheets": sheets,
|
||||
});
|
||||
|
||||
let body_str = serde_json::to_string(&body).map_err(|e| e.to_string())?;
|
||||
let response = api_call("POST", "", Some(&body_str))?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(CreateSpreadsheetResult {
|
||||
spreadsheet_id: parsed["spreadsheetId"].as_str().unwrap_or("").to_string(),
|
||||
title: parsed["properties"]["title"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
url: parsed["spreadsheetUrl"].as_str().unwrap_or("").to_string(),
|
||||
sheets: parsed["sheets"]
|
||||
.as_array()
|
||||
.map(|arr| arr.iter().map(parse_sheet_info).collect())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get spreadsheet metadata.
|
||||
pub fn get_spreadsheet(spreadsheet_id: &str) -> Result<SpreadsheetMetadata, String> {
|
||||
let path = format!(
|
||||
"{}?fields=spreadsheetId,properties.title,spreadsheetUrl,sheets.properties,namedRanges",
|
||||
url_encode(spreadsheet_id)
|
||||
);
|
||||
|
||||
let response = api_call("GET", &path, None)?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(SpreadsheetMetadata {
|
||||
spreadsheet_id: parsed["spreadsheetId"].as_str().unwrap_or("").to_string(),
|
||||
title: parsed["properties"]["title"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
url: parsed["spreadsheetUrl"].as_str().unwrap_or("").to_string(),
|
||||
sheets: parsed["sheets"]
|
||||
.as_array()
|
||||
.map(|arr| arr.iter().map(parse_sheet_info).collect())
|
||||
.unwrap_or_default(),
|
||||
named_ranges: parsed["namedRanges"]
|
||||
.as_array()
|
||||
.map(|arr| arr.iter().map(parse_named_range).collect())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Read values from a single range.
|
||||
pub fn read_values(spreadsheet_id: &str, range: &str) -> Result<ValuesResult, String> {
|
||||
let path = format!(
|
||||
"{}/values/{}",
|
||||
url_encode(spreadsheet_id),
|
||||
url_encode(range)
|
||||
);
|
||||
|
||||
let response = api_call("GET", &path, None)?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(ValuesResult {
|
||||
range: parsed["range"].as_str().unwrap_or("").to_string(),
|
||||
values: parsed["values"]
|
||||
.as_array()
|
||||
.map(|rows| {
|
||||
rows.iter()
|
||||
.map(|row| row.as_array().map(|cols| cols.to_vec()).unwrap_or_default())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Read values from multiple ranges at once.
|
||||
pub fn batch_read_values(
|
||||
spreadsheet_id: &str,
|
||||
ranges: &[String],
|
||||
) -> Result<BatchValuesResult, String> {
|
||||
let range_params: Vec<String> = ranges
|
||||
.iter()
|
||||
.map(|r| format!("ranges={}", url_encode(r)))
|
||||
.collect();
|
||||
|
||||
let path = format!(
|
||||
"{}/values:batchGet?{}",
|
||||
url_encode(spreadsheet_id),
|
||||
range_params.join("&")
|
||||
);
|
||||
|
||||
let response = api_call("GET", &path, None)?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let value_ranges = parsed["valueRanges"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|vr| ValuesResult {
|
||||
range: vr["range"].as_str().unwrap_or("").to_string(),
|
||||
values: vr["values"]
|
||||
.as_array()
|
||||
.map(|rows| {
|
||||
rows.iter()
|
||||
.map(|row| {
|
||||
row.as_array().map(|cols| cols.to_vec()).unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(BatchValuesResult { value_ranges })
|
||||
}
|
||||
|
||||
/// Write values to a range.
|
||||
pub fn write_values(
|
||||
spreadsheet_id: &str,
|
||||
range: &str,
|
||||
values: &[Vec<serde_json::Value>],
|
||||
value_input_option: &str,
|
||||
) -> Result<UpdateResult, String> {
|
||||
let path = format!(
|
||||
"{}/values/{}?valueInputOption={}",
|
||||
url_encode(spreadsheet_id),
|
||||
url_encode(range),
|
||||
url_encode(value_input_option)
|
||||
);
|
||||
|
||||
let body = serde_json::json!({
|
||||
"range": range,
|
||||
"majorDimension": "ROWS",
|
||||
"values": values,
|
||||
});
|
||||
|
||||
let body_str = serde_json::to_string(&body).map_err(|e| e.to_string())?;
|
||||
let response = api_call("PUT", &path, Some(&body_str))?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(UpdateResult {
|
||||
updated_range: parsed["updatedRange"].as_str().unwrap_or("").to_string(),
|
||||
updated_rows: parsed["updatedRows"].as_i64().unwrap_or(0),
|
||||
updated_columns: parsed["updatedColumns"].as_i64().unwrap_or(0),
|
||||
updated_cells: parsed["updatedCells"].as_i64().unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Append rows after existing data.
|
||||
pub fn append_values(
|
||||
spreadsheet_id: &str,
|
||||
range: &str,
|
||||
values: &[Vec<serde_json::Value>],
|
||||
value_input_option: &str,
|
||||
) -> Result<UpdateResult, String> {
|
||||
let path = format!(
|
||||
"{}/values/{}:append?valueInputOption={}&insertDataOption=INSERT_ROWS",
|
||||
url_encode(spreadsheet_id),
|
||||
url_encode(range),
|
||||
url_encode(value_input_option)
|
||||
);
|
||||
|
||||
let body = serde_json::json!({
|
||||
"range": range,
|
||||
"majorDimension": "ROWS",
|
||||
"values": values,
|
||||
});
|
||||
|
||||
let body_str = serde_json::to_string(&body).map_err(|e| e.to_string())?;
|
||||
let response = api_call("POST", &path, Some(&body_str))?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let updates = &parsed["updates"];
|
||||
Ok(UpdateResult {
|
||||
updated_range: updates["updatedRange"].as_str().unwrap_or("").to_string(),
|
||||
updated_rows: updates["updatedRows"].as_i64().unwrap_or(0),
|
||||
updated_columns: updates["updatedColumns"].as_i64().unwrap_or(0),
|
||||
updated_cells: updates["updatedCells"].as_i64().unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear values from a range.
|
||||
pub fn clear_values(spreadsheet_id: &str, range: &str) -> Result<ClearResult, String> {
|
||||
let path = format!(
|
||||
"{}/values/{}:clear",
|
||||
url_encode(spreadsheet_id),
|
||||
url_encode(range)
|
||||
);
|
||||
|
||||
let response = api_call("POST", &path, Some("{}"))?;
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(ClearResult {
|
||||
cleared_range: parsed["clearedRange"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a batchUpdate request to the spreadsheet.
|
||||
fn batch_update(
|
||||
spreadsheet_id: &str,
|
||||
requests: Vec<serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let path = format!("{}:batchUpdate", url_encode(spreadsheet_id));
|
||||
|
||||
let body = serde_json::json!({ "requests": requests });
|
||||
let body_str = serde_json::to_string(&body).map_err(|e| e.to_string())?;
|
||||
|
||||
let response = api_call("POST", &path, Some(&body_str))?;
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))
|
||||
}
|
||||
|
||||
/// Add a new sheet (tab) to the spreadsheet.
|
||||
pub fn add_sheet(spreadsheet_id: &str, title: &str) -> Result<AddSheetResult, String> {
|
||||
let requests = vec![serde_json::json!({
|
||||
"addSheet": {
|
||||
"properties": {
|
||||
"title": title
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
let parsed = batch_update(spreadsheet_id, requests)?;
|
||||
|
||||
let reply = &parsed["replies"][0]["addSheet"]["properties"];
|
||||
Ok(AddSheetResult {
|
||||
sheet: SheetInfo {
|
||||
sheet_id: reply["sheetId"].as_i64().unwrap_or(0),
|
||||
title: reply["title"].as_str().unwrap_or("").to_string(),
|
||||
index: reply["index"].as_i64().unwrap_or(0),
|
||||
row_count: reply["gridProperties"]["rowCount"].as_i64().unwrap_or(1000),
|
||||
column_count: reply["gridProperties"]["columnCount"]
|
||||
.as_i64()
|
||||
.unwrap_or(26),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a sheet (tab) from the spreadsheet.
|
||||
pub fn delete_sheet(spreadsheet_id: &str, sheet_id: i64) -> Result<SheetOperationResult, String> {
|
||||
let requests = vec![serde_json::json!({
|
||||
"deleteSheet": {
|
||||
"sheetId": sheet_id
|
||||
}
|
||||
})];
|
||||
|
||||
batch_update(spreadsheet_id, requests)?;
|
||||
|
||||
Ok(SheetOperationResult {
|
||||
spreadsheet_id: spreadsheet_id.to_string(),
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Rename a sheet (tab).
|
||||
pub fn rename_sheet(
|
||||
spreadsheet_id: &str,
|
||||
sheet_id: i64,
|
||||
title: &str,
|
||||
) -> Result<SheetOperationResult, String> {
|
||||
let requests = vec![serde_json::json!({
|
||||
"updateSheetProperties": {
|
||||
"properties": {
|
||||
"sheetId": sheet_id,
|
||||
"title": title
|
||||
},
|
||||
"fields": "title"
|
||||
}
|
||||
})];
|
||||
|
||||
batch_update(spreadsheet_id, requests)?;
|
||||
|
||||
Ok(SheetOperationResult {
|
||||
spreadsheet_id: spreadsheet_id.to_string(),
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a hex color like "#FF0000" into Sheets API color (0.0-1.0 floats).
|
||||
fn parse_hex_color(hex: &str) -> Option<serde_json::Value> {
|
||||
let hex = hex.strip_prefix('#').unwrap_or(hex);
|
||||
if hex.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
Some(serde_json::json!({
|
||||
"red": r as f64 / 255.0,
|
||||
"green": g as f64 / 255.0,
|
||||
"blue": b as f64 / 255.0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parameters for cell formatting.
|
||||
pub struct FormatOptions<'a> {
|
||||
pub spreadsheet_id: &'a str,
|
||||
pub sheet_id: i64,
|
||||
pub start_row: i64,
|
||||
pub end_row: i64,
|
||||
pub start_column: i64,
|
||||
pub end_column: i64,
|
||||
pub bold: Option<bool>,
|
||||
pub italic: Option<bool>,
|
||||
pub font_size: Option<i64>,
|
||||
pub text_color: Option<&'a str>,
|
||||
pub background_color: Option<&'a str>,
|
||||
pub horizontal_alignment: Option<&'a str>,
|
||||
pub number_format: Option<&'a str>,
|
||||
pub number_format_type: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Format cells in a range.
|
||||
pub fn format_cells(opts: FormatOptions<'_>) -> Result<FormatResult, String> {
|
||||
let mut format = serde_json::json!({});
|
||||
let mut fields = Vec::new();
|
||||
|
||||
// Text format
|
||||
let mut text_format = serde_json::json!({});
|
||||
let mut has_text_format = false;
|
||||
|
||||
if let Some(b) = opts.bold {
|
||||
text_format["bold"] = serde_json::Value::Bool(b);
|
||||
has_text_format = true;
|
||||
}
|
||||
if let Some(i) = opts.italic {
|
||||
text_format["italic"] = serde_json::Value::Bool(i);
|
||||
has_text_format = true;
|
||||
}
|
||||
if let Some(size) = opts.font_size {
|
||||
text_format["fontSize"] = serde_json::json!(size);
|
||||
has_text_format = true;
|
||||
}
|
||||
if let Some(color) = opts.text_color {
|
||||
if let Some(c) = parse_hex_color(color) {
|
||||
text_format["foregroundColor"] = c;
|
||||
has_text_format = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_text_format {
|
||||
format["textFormat"] = text_format;
|
||||
fields.push("userEnteredFormat.textFormat");
|
||||
}
|
||||
|
||||
// Background color
|
||||
if let Some(color) = opts.background_color {
|
||||
if let Some(c) = parse_hex_color(color) {
|
||||
format["backgroundColor"] = c;
|
||||
fields.push("userEnteredFormat.backgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal alignment
|
||||
if let Some(align) = opts.horizontal_alignment {
|
||||
format["horizontalAlignment"] = serde_json::Value::String(align.to_string());
|
||||
fields.push("userEnteredFormat.horizontalAlignment");
|
||||
}
|
||||
|
||||
// Number format
|
||||
if let Some(pattern) = opts.number_format {
|
||||
let fmt_type = opts.number_format_type.unwrap_or("NUMBER");
|
||||
format["numberFormat"] = serde_json::json!({
|
||||
"type": fmt_type,
|
||||
"pattern": pattern,
|
||||
});
|
||||
fields.push("userEnteredFormat.numberFormat");
|
||||
}
|
||||
|
||||
if fields.is_empty() {
|
||||
return Err("No formatting options specified".to_string());
|
||||
}
|
||||
|
||||
let requests = vec![serde_json::json!({
|
||||
"repeatCell": {
|
||||
"range": {
|
||||
"sheetId": opts.sheet_id,
|
||||
"startRowIndex": opts.start_row,
|
||||
"endRowIndex": opts.end_row,
|
||||
"startColumnIndex": opts.start_column,
|
||||
"endColumnIndex": opts.end_column,
|
||||
},
|
||||
"cell": {
|
||||
"userEnteredFormat": format,
|
||||
},
|
||||
"fields": fields.join(","),
|
||||
}
|
||||
})];
|
||||
|
||||
batch_update(opts.spreadsheet_id, requests)?;
|
||||
|
||||
Ok(FormatResult {
|
||||
spreadsheet_id: opts.spreadsheet_id.to_string(),
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Minimal percent-encoding for URL path segments and query values.
|
||||
fn url_encode(s: &str) -> String {
|
||||
let mut encoded = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
encoded.push(b as char);
|
||||
}
|
||||
_ => {
|
||||
encoded.push('%');
|
||||
encoded.push(char::from(HEX[(b >> 4) as usize]));
|
||||
encoded.push(char::from(HEX[(b & 0x0F) as usize]));
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
const HEX: [u8; 16] = *b"0123456789ABCDEF";
|
||||
@@ -0,0 +1,454 @@
|
||||
//! Google Sheets WASM Tool for IronClaw.
|
||||
//!
|
||||
//! Provides Google Sheets integration for creating, reading, writing,
|
||||
//! and formatting spreadsheets. Use Google Drive tool to search for
|
||||
//! existing spreadsheets by name.
|
||||
//!
|
||||
//! # Capabilities Required
|
||||
//!
|
||||
//! - HTTP: `sheets.googleapis.com/v4/spreadsheets*`
|
||||
//! - Secrets: `google_oauth_token` (shared OAuth 2.0 token, injected automatically)
|
||||
//!
|
||||
//! # Supported Actions
|
||||
//!
|
||||
//! - `create_spreadsheet`: Create a new spreadsheet with optional sheet names
|
||||
//! - `get_spreadsheet`: Get metadata (title, sheets, named ranges)
|
||||
//! - `read_values`: Read cell values from a range (A1 notation)
|
||||
//! - `batch_read_values`: Read from multiple ranges at once
|
||||
//! - `write_values`: Write values to a range (overwrites)
|
||||
//! - `append_values`: Append rows after existing data
|
||||
//! - `clear_values`: Clear values from a range (keeps formatting)
|
||||
//! - `add_sheet`: Add a new sheet (tab)
|
||||
//! - `delete_sheet`: Delete a sheet (tab)
|
||||
//! - `rename_sheet`: Rename a sheet (tab)
|
||||
//! - `format_cells`: Format cells (bold, colors, alignment, number format)
|
||||
//!
|
||||
//! # Tips
|
||||
//!
|
||||
//! - Spreadsheet IDs are the same as Google Drive file IDs. Use google-drive
|
||||
//! tool's list_files to find spreadsheets.
|
||||
//! - Use A1 notation for ranges: "Sheet1!A1:D10", "A1:B5", "Sheet1!A:E"
|
||||
//! - Sheet IDs (numeric) are different from sheet names. Get them via get_spreadsheet.
|
||||
//!
|
||||
//! # Example Usage
|
||||
//!
|
||||
//! ```json
|
||||
//! {"action": "create_spreadsheet", "title": "Q1 Report", "sheet_names": ["Revenue", "Expenses"]}
|
||||
//! {"action": "read_values", "spreadsheet_id": "abc123", "range": "Sheet1!A1:D10"}
|
||||
//! {"action": "write_values", "spreadsheet_id": "abc123", "range": "Sheet1!A1", "values": [["Name", "Age"], ["Alice", 30]]}
|
||||
//! {"action": "append_values", "spreadsheet_id": "abc123", "range": "Sheet1!A:B", "values": [["Bob", 25]]}
|
||||
//! {"action": "format_cells", "spreadsheet_id": "abc123", "sheet_id": 0, "start_row": 0, "end_row": 1, "start_column": 0, "end_column": 4, "bold": true, "background_color": "#4285F4", "text_color": "#FFFFFF"}
|
||||
//! ```
|
||||
|
||||
mod api;
|
||||
mod types;
|
||||
|
||||
use types::GoogleSheetsAction;
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-tool",
|
||||
path: "../../wit/tool.wit",
|
||||
});
|
||||
|
||||
struct GoogleSheetsTool;
|
||||
|
||||
impl exports::near::agent::tool::Guest for GoogleSheetsTool {
|
||||
fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response {
|
||||
match execute_inner(&req.params) {
|
||||
Ok(result) => exports::near::agent::tool::Response {
|
||||
output: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => exports::near::agent::tool::Response {
|
||||
output: None,
|
||||
error: Some(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> String {
|
||||
r#"{
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "create_spreadsheet" },
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Spreadsheet title"
|
||||
},
|
||||
"sheet_names": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Names for sheets (tabs). Defaults to ['Sheet1'] if omitted."
|
||||
}
|
||||
},
|
||||
"required": ["action", "title"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_spreadsheet" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID (same as Google Drive file ID)"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "read_values" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"range": {
|
||||
"type": "string",
|
||||
"description": "A1 notation range (e.g., 'Sheet1!A1:D10', 'A1:B5')"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "range"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "batch_read_values" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"ranges": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of A1 notation ranges to read"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "ranges"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "write_values" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"range": {
|
||||
"type": "string",
|
||||
"description": "A1 notation range (e.g., 'Sheet1!A1')"
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": { "type": "array" },
|
||||
"description": "2D array of values (rows of columns)"
|
||||
},
|
||||
"value_input_option": {
|
||||
"type": "string",
|
||||
"enum": ["RAW", "USER_ENTERED"],
|
||||
"description": "How to interpret input. USER_ENTERED (default) parses like typing in the UI. RAW stores as-is.",
|
||||
"default": "USER_ENTERED"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "range", "values"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "append_values" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"range": {
|
||||
"type": "string",
|
||||
"description": "A1 notation range to find the table (e.g., 'Sheet1!A:E')"
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": { "type": "array" },
|
||||
"description": "Rows to append (2D array)"
|
||||
},
|
||||
"value_input_option": {
|
||||
"type": "string",
|
||||
"enum": ["RAW", "USER_ENTERED"],
|
||||
"description": "How to interpret input (default: USER_ENTERED)",
|
||||
"default": "USER_ENTERED"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "range", "values"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "clear_values" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"range": {
|
||||
"type": "string",
|
||||
"description": "A1 notation range to clear"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "range"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "add_sheet" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Name for the new sheet (tab)"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "title"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "delete_sheet" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"sheet_id": {
|
||||
"type": "integer",
|
||||
"description": "Numeric sheet ID (get from get_spreadsheet, NOT the sheet name)"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "sheet_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "rename_sheet" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"sheet_id": {
|
||||
"type": "integer",
|
||||
"description": "Numeric sheet ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "New name for the sheet"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "sheet_id", "title"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "format_cells" },
|
||||
"spreadsheet_id": {
|
||||
"type": "string",
|
||||
"description": "The spreadsheet ID"
|
||||
},
|
||||
"sheet_id": {
|
||||
"type": "integer",
|
||||
"description": "Numeric sheet ID"
|
||||
},
|
||||
"start_row": {
|
||||
"type": "integer",
|
||||
"description": "Start row (0-indexed, inclusive)"
|
||||
},
|
||||
"end_row": {
|
||||
"type": "integer",
|
||||
"description": "End row (0-indexed, exclusive)"
|
||||
},
|
||||
"start_column": {
|
||||
"type": "integer",
|
||||
"description": "Start column (0-indexed, inclusive)"
|
||||
},
|
||||
"end_column": {
|
||||
"type": "integer",
|
||||
"description": "End column (0-indexed, exclusive)"
|
||||
},
|
||||
"bold": {
|
||||
"type": "boolean",
|
||||
"description": "Make text bold"
|
||||
},
|
||||
"italic": {
|
||||
"type": "boolean",
|
||||
"description": "Make text italic"
|
||||
},
|
||||
"font_size": {
|
||||
"type": "integer",
|
||||
"description": "Font size in points"
|
||||
},
|
||||
"text_color": {
|
||||
"type": "string",
|
||||
"description": "Text color as hex (e.g., '#FF0000' for red)"
|
||||
},
|
||||
"background_color": {
|
||||
"type": "string",
|
||||
"description": "Cell background color as hex (e.g., '#FFFF00' for yellow)"
|
||||
},
|
||||
"horizontal_alignment": {
|
||||
"type": "string",
|
||||
"enum": ["LEFT", "CENTER", "RIGHT"],
|
||||
"description": "Horizontal text alignment"
|
||||
},
|
||||
"number_format": {
|
||||
"type": "string",
|
||||
"description": "Number format pattern (e.g., '#,##0.00', 'yyyy-mm-dd', '$#,##0')"
|
||||
},
|
||||
"number_format_type": {
|
||||
"type": "string",
|
||||
"enum": ["NUMBER", "CURRENCY", "PERCENT", "DATE", "TIME", "TEXT"],
|
||||
"description": "Type of number format (default: NUMBER)"
|
||||
}
|
||||
},
|
||||
"required": ["action", "spreadsheet_id", "sheet_id", "start_row", "end_row", "start_column", "end_column"]
|
||||
}
|
||||
]
|
||||
}"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"Google Sheets integration for creating, reading, writing, and formatting spreadsheets. \
|
||||
Supports cell value operations (read, write, append, clear) using A1 notation, sheet \
|
||||
(tab) management (add, delete, rename), and cell formatting (bold, colors, alignment, \
|
||||
number formats). Spreadsheet IDs are the same as Google Drive file IDs, so use the \
|
||||
google-drive tool to search for existing spreadsheets. Requires a Google OAuth token \
|
||||
with the spreadsheets scope."
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_inner(params: &str) -> Result<String, String> {
|
||||
if !crate::near::agent::host::secret_exists("google_oauth_token") {
|
||||
return Err(
|
||||
"Google OAuth token not configured. Run `ironclaw tool auth google-sheets` to set up \
|
||||
OAuth, or set the GOOGLE_OAUTH_TOKEN environment variable."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let action: GoogleSheetsAction =
|
||||
serde_json::from_str(params).map_err(|e| format!("Invalid parameters: {}", e))?;
|
||||
|
||||
crate::near::agent::host::log(
|
||||
crate::near::agent::host::LogLevel::Info,
|
||||
&format!("Executing Google Sheets action: {:?}", action),
|
||||
);
|
||||
|
||||
let result = match action {
|
||||
GoogleSheetsAction::CreateSpreadsheet { title, sheet_names } => {
|
||||
let result = api::create_spreadsheet(&title, &sheet_names)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::GetSpreadsheet { spreadsheet_id } => {
|
||||
let result = api::get_spreadsheet(&spreadsheet_id)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::ReadValues {
|
||||
spreadsheet_id,
|
||||
range,
|
||||
} => {
|
||||
let result = api::read_values(&spreadsheet_id, &range)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::BatchReadValues {
|
||||
spreadsheet_id,
|
||||
ranges,
|
||||
} => {
|
||||
let result = api::batch_read_values(&spreadsheet_id, &ranges)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::WriteValues {
|
||||
spreadsheet_id,
|
||||
range,
|
||||
values,
|
||||
value_input_option,
|
||||
} => {
|
||||
let result = api::write_values(&spreadsheet_id, &range, &values, &value_input_option)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::AppendValues {
|
||||
spreadsheet_id,
|
||||
range,
|
||||
values,
|
||||
value_input_option,
|
||||
} => {
|
||||
let result = api::append_values(&spreadsheet_id, &range, &values, &value_input_option)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::ClearValues {
|
||||
spreadsheet_id,
|
||||
range,
|
||||
} => {
|
||||
let result = api::clear_values(&spreadsheet_id, &range)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::AddSheet {
|
||||
spreadsheet_id,
|
||||
title,
|
||||
} => {
|
||||
let result = api::add_sheet(&spreadsheet_id, &title)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::DeleteSheet {
|
||||
spreadsheet_id,
|
||||
sheet_id,
|
||||
} => {
|
||||
let result = api::delete_sheet(&spreadsheet_id, sheet_id)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::RenameSheet {
|
||||
spreadsheet_id,
|
||||
sheet_id,
|
||||
title,
|
||||
} => {
|
||||
let result = api::rename_sheet(&spreadsheet_id, sheet_id, &title)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
GoogleSheetsAction::FormatCells {
|
||||
spreadsheet_id,
|
||||
sheet_id,
|
||||
start_row,
|
||||
end_row,
|
||||
start_column,
|
||||
end_column,
|
||||
bold,
|
||||
italic,
|
||||
font_size,
|
||||
text_color,
|
||||
background_color,
|
||||
horizontal_alignment,
|
||||
number_format,
|
||||
number_format_type,
|
||||
} => {
|
||||
let result = api::format_cells(api::FormatOptions {
|
||||
spreadsheet_id: &spreadsheet_id,
|
||||
sheet_id,
|
||||
start_row,
|
||||
end_row,
|
||||
start_column,
|
||||
end_column,
|
||||
bold,
|
||||
italic,
|
||||
font_size,
|
||||
text_color: text_color.as_deref(),
|
||||
background_color: background_color.as_deref(),
|
||||
horizontal_alignment: horizontal_alignment.as_deref(),
|
||||
number_format: number_format.as_deref(),
|
||||
number_format_type: number_format_type.as_deref(),
|
||||
})?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
export!(GoogleSheetsTool);
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Types for Google Sheets API requests and responses.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input parameters for the Google Sheets tool.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum GoogleSheetsAction {
|
||||
/// Create a new spreadsheet.
|
||||
CreateSpreadsheet {
|
||||
/// Spreadsheet title.
|
||||
title: String,
|
||||
/// Names of sheets (tabs) to create. Defaults to one sheet named "Sheet1".
|
||||
#[serde(default)]
|
||||
sheet_names: Vec<String>,
|
||||
},
|
||||
|
||||
/// Get spreadsheet metadata (title, sheets, named ranges).
|
||||
GetSpreadsheet {
|
||||
/// The spreadsheet ID (same as Google Drive file ID).
|
||||
spreadsheet_id: String,
|
||||
},
|
||||
|
||||
/// Read cell values from a range.
|
||||
ReadValues {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// A1 notation range (e.g., "Sheet1!A1:D10", "A1:B5").
|
||||
range: String,
|
||||
},
|
||||
|
||||
/// Read values from multiple ranges at once.
|
||||
BatchReadValues {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// List of A1 notation ranges.
|
||||
ranges: Vec<String>,
|
||||
},
|
||||
|
||||
/// Write values to a range (overwrites existing data).
|
||||
WriteValues {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// A1 notation range (e.g., "Sheet1!A1:D10").
|
||||
range: String,
|
||||
/// 2D array of values (rows of columns).
|
||||
values: Vec<Vec<serde_json::Value>>,
|
||||
/// How to interpret input: "RAW" or "USER_ENTERED" (default).
|
||||
#[serde(default = "default_value_input_option")]
|
||||
value_input_option: String,
|
||||
},
|
||||
|
||||
/// Append rows after existing data in a range.
|
||||
AppendValues {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// A1 notation range to search for a table (e.g., "Sheet1!A:E").
|
||||
range: String,
|
||||
/// Rows to append (2D array).
|
||||
values: Vec<Vec<serde_json::Value>>,
|
||||
/// How to interpret input: "RAW" or "USER_ENTERED" (default).
|
||||
#[serde(default = "default_value_input_option")]
|
||||
value_input_option: String,
|
||||
},
|
||||
|
||||
/// Clear values from a range (keeps formatting).
|
||||
ClearValues {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// A1 notation range to clear.
|
||||
range: String,
|
||||
},
|
||||
|
||||
/// Add a new sheet (tab) to the spreadsheet.
|
||||
AddSheet {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// Name for the new sheet.
|
||||
title: String,
|
||||
},
|
||||
|
||||
/// Delete a sheet (tab) from the spreadsheet.
|
||||
DeleteSheet {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// Numeric sheet ID (from get_spreadsheet, NOT the sheet name).
|
||||
sheet_id: i64,
|
||||
},
|
||||
|
||||
/// Rename a sheet (tab).
|
||||
RenameSheet {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// Numeric sheet ID.
|
||||
sheet_id: i64,
|
||||
/// New name for the sheet.
|
||||
title: String,
|
||||
},
|
||||
|
||||
/// Format cells in a range (bold, colors, number format, borders, alignment).
|
||||
FormatCells {
|
||||
/// The spreadsheet ID.
|
||||
spreadsheet_id: String,
|
||||
/// Numeric sheet ID.
|
||||
sheet_id: i64,
|
||||
/// Start row (0-indexed, inclusive).
|
||||
start_row: i64,
|
||||
/// End row (0-indexed, exclusive).
|
||||
end_row: i64,
|
||||
/// Start column (0-indexed, inclusive).
|
||||
start_column: i64,
|
||||
/// End column (0-indexed, exclusive).
|
||||
end_column: i64,
|
||||
/// Bold text.
|
||||
#[serde(default)]
|
||||
bold: Option<bool>,
|
||||
/// Italic text.
|
||||
#[serde(default)]
|
||||
italic: Option<bool>,
|
||||
/// Font size.
|
||||
#[serde(default)]
|
||||
font_size: Option<i64>,
|
||||
/// Text color as hex (e.g., "#FF0000").
|
||||
#[serde(default)]
|
||||
text_color: Option<String>,
|
||||
/// Background color as hex (e.g., "#FFFF00").
|
||||
#[serde(default)]
|
||||
background_color: Option<String>,
|
||||
/// Horizontal alignment: "LEFT", "CENTER", "RIGHT".
|
||||
#[serde(default)]
|
||||
horizontal_alignment: Option<String>,
|
||||
/// Number format pattern (e.g., "#,##0.00", "yyyy-mm-dd").
|
||||
#[serde(default)]
|
||||
number_format: Option<String>,
|
||||
/// Number format type: "NUMBER", "CURRENCY", "PERCENT", "DATE", "TIME", "TEXT".
|
||||
#[serde(default)]
|
||||
number_format_type: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn default_value_input_option() -> String {
|
||||
"USER_ENTERED".to_string()
|
||||
}
|
||||
|
||||
/// Sheet (tab) info within a spreadsheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SheetInfo {
|
||||
pub sheet_id: i64,
|
||||
pub title: String,
|
||||
pub index: i64,
|
||||
pub row_count: i64,
|
||||
pub column_count: i64,
|
||||
}
|
||||
|
||||
/// Named range within a spreadsheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NamedRange {
|
||||
pub named_range_id: String,
|
||||
pub name: String,
|
||||
pub range: String,
|
||||
}
|
||||
|
||||
/// Result from create_spreadsheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateSpreadsheetResult {
|
||||
pub spreadsheet_id: String,
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub sheets: Vec<SheetInfo>,
|
||||
}
|
||||
|
||||
/// Result from get_spreadsheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SpreadsheetMetadata {
|
||||
pub spreadsheet_id: String,
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub sheets: Vec<SheetInfo>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub named_ranges: Vec<NamedRange>,
|
||||
}
|
||||
|
||||
/// Result from read_values.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ValuesResult {
|
||||
pub range: String,
|
||||
pub values: Vec<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// Result from batch_read_values.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchValuesResult {
|
||||
pub value_ranges: Vec<ValuesResult>,
|
||||
}
|
||||
|
||||
/// Result from write_values or append_values.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateResult {
|
||||
pub updated_range: String,
|
||||
pub updated_rows: i64,
|
||||
pub updated_columns: i64,
|
||||
pub updated_cells: i64,
|
||||
}
|
||||
|
||||
/// Result from clear_values.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ClearResult {
|
||||
pub cleared_range: String,
|
||||
}
|
||||
|
||||
/// Result from add_sheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AddSheetResult {
|
||||
pub sheet: SheetInfo,
|
||||
}
|
||||
|
||||
/// Result from delete_sheet or rename_sheet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SheetOperationResult {
|
||||
pub spreadsheet_id: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// Result from format_cells.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FormatResult {
|
||||
pub spreadsheet_id: String,
|
||||
pub success: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user