From ea8b2812d46a4c558e7e0093a9c84c3ecd29d9c0 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 2 Feb 2026 09:18:54 +0800 Subject: [PATCH] add ezBookkeeping API tools --- scripts/ebktools.ps1 | 1112 ++++++++++++++++++++++++++++++++++++++++++ scripts/ebktools.sh | 949 +++++++++++++++++++++++++++++++++++ 2 files changed, 2061 insertions(+) create mode 100755 scripts/ebktools.ps1 create mode 100755 scripts/ebktools.sh diff --git a/scripts/ebktools.ps1 b/scripts/ebktools.ps1 new file mode 100755 index 00000000..139d3a25 --- /dev/null +++ b/scripts/ebktools.ps1 @@ -0,0 +1,1112 @@ +#!/usr/bin/env pwsh + +# ezBookkeeping API Tools +# A command-line tool for calling ezBookkeeping APIs + +param( + [Parameter(Position=0)] + [string]$Command = "", + + [Parameter(Mandatory=$false)] + [string]$tzName = "", + + [Parameter(Mandatory=$false)] + [string]$tzOffset = "", + + [Parameter(ValueFromRemainingArguments=$true)] + [string[]]$CommandArgs +) + +# API Configuration Structure +$API_CONFIGS = @( + @{ + Name = "tokens-list" + Description = "Get available sessions information" + Method = "GET" + Path = "tokens/list.json" + RequiresTimezone = $false + RequiredParams = @() + OptionalParams = @() + ParamTypes = @{} + ParamDescriptions = @{} + ResponseStructure = @( + "[" + " {" + " `"tokenId`": `"string (Token ID)`"," + " `"tokenType`": `"integer (Token type, 1: Normal Token, 5: MCP Token, 8: API Token)`"," + " `"userAgent`": `"string (The User Agent when the session created)`"," + " `"lastSeen`": `"integer (Last refresh unix time of the session)`"," + " `"isCurrent`": `"boolean (Whether the session is current)`"" + " }" + "]" + ) + } + @{ + Name = "tokens-revoke" + Description = "Revoke token" + Method = "POST" + Path = "tokens/revoke.json" + RequiresTimezone = $false + RequiredParams = @("tokenId") + OptionalParams = @() + ParamTypes = @{ + "tokenId" = "string" + } + ParamDescriptions = @{ + "tokenId" = "string (Token ID)" + } + ResponseStructure = @( + "boolean (Whether the token is revoked successfully)" + ) + } + @{ + Name = "accounts-list" + Description = "Get all accounts list" + Method = "GET" + Path = "accounts/list.json" + RequiresTimezone = $false + RequiredParams = @() + OptionalParams = @() + ParamTypes = @{} + ParamDescriptions = @{} + ResponseStructure = @( + "[" + " {" + " `"id`": `"string (Account ID)`"," + " `"name`": `"string (Account name)`"," + " `"parentId`": `"string (Parent account ID)`"," + " `"category`": `"integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)`"," + " `"type`": `"integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)`"," + " `"icon`": `"string (Account icon ID)`"," + " `"color`": `"string (Account icon color, Hex color code RRGGBB)`"," + " `"currency`": `"string (Account currency code)`"," + " `"balance`": `"integer (Account balance, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`")`"," + " `"comment`": `"string (Account description)`"," + " `"creditCardStatementDate`": `"integer (The statement date of the credit card account)`"," + " `"displayOrder`": `"integer (The display order of the account)`"," + " `"isAsset`": `"boolean (Whether the account is an asset account)`"," + " `"isLiability`": `"boolean (Whether the account is a liability account)`"," + " `"hidden`": `"boolean (Whether the account is hidden)`"," + " `"subAccounts`": [`"each sub-account object like an account object`"]" + " }" + "]" + ) + } + @{ + Name = "accounts-add" + Description = "Add account" + Method = "POST" + Path = "accounts/add.json" + RequiresTimezone = $true + RequiredParams = @("name", "category", "type", "icon", "color", "currency") + OptionalParams = @("balance", "balanceTime", "comment", "creditCardStatementDate") + ParamTypes = @{ + "name" = "string" + "category" = "integer" + "type" = "integer" + "icon" = "string" + "color" = "string" + "currency" = "string" + "balance" = "integer" + "balanceTime" = "integer" + "comment" = "string" + "creditCardStatementDate" = "integer" + } + ParamDescriptions = @{ + "name" = "string (Account name)" + "category" = "integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)" + "type" = "integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)" + "icon" = "string (Account icon ID)" + "color" = "string (Account icon color, hex color code RRGGBB)" + "currency" = "string (Account currency code, ISO 4217 code, `"---`" for the parent account)" + "balance" = "integer (Account balance, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`". Liability account should set to negative amount)" + "balanceTime" = "integer (The unix time when the account balance is the set value. This field is required when balance is set)" + "comment" = "string (Account description)" + "creditCardStatementDate" = "integer (The statement date of the credit card account)" + } + ResponseStructure = @( + "{" + " `"id`": `"string (Account ID)`"," + " `"name`": `"string (Account name)`"," + " `"parentId`": `"string (Parent account ID)`"," + " `"category`": `"integer (Account category)`"," + " `"type`": `"integer (Account type)`"," + " `"icon`": `"string (Account icon ID)`"," + " `"color`": `"string (Account icon color)`"," + " `"currency`": `"string (Account currency code)`"," + " `"balance`": `"integer (Account balance)`"," + " `"comment`": `"string (Account description)`"," + " `"creditCardStatementDate`": `"integer (The statement date of the credit card account)`"," + " `"displayOrder`": `"integer (The display order of the account)`"," + " `"isAsset`": `"boolean (Whether the account is an asset account)`"," + " `"isLiability`": `"boolean (Whether the account is a liability account)`"," + " `"hidden`": `"boolean (Whether the account is hidden)`"," + " `"subAccounts`": [`"every sub-account object like account object`"]" + "}" + ) + } + @{ + Name = "transaction-categories-list" + Description = "Get all transaction categories" + Method = "GET" + Path = "transaction/categories/list.json" + RequiresTimezone = $false + RequiredParams = @() + OptionalParams = @() + ParamTypes = @{} + ParamDescriptions = @{} + ResponseStructure = @( + "{" + " `"transaction category type (1: Income, 2: Expense, 3:Transfer)`": [" + " {" + " `"id`": `"string (Transaction category ID)`"," + " `"name`": `"string (Transaction category name)`"," + " `"parentId`": `"string (Parent transaction category ID)`"," + " `"type`": `"integer (Transaction category type)`"," + " `"icon`": `"string (Transaction category icon ID)`"," + " `"color`": `"string (Transaction category icon color, hex color code RRGGBB)`"," + " `"comment`": `"string (Transaction category description)`"," + " `"displayOrder`": `"integer (The display order of the transaction category)`"," + " `"hidden`": `"boolean (Whether the transaction category is hidden)`"," + " `"subCategories`": [`"each sub-category object like a transaction category object`"]" + " }" + " ]" + "}" + ) + } + @{ + Name = "transaction-categories-add" + Description = "Add transaction category" + Method = "POST" + Path = "transaction/categories/add.json" + RequiresTimezone = $false + RequiredParams = @("name", "type", "icon", "color") + OptionalParams = @("parentId", "comment") + ParamTypes = @{ + "name" = "string" + "type" = "integer" + "parentId" = "string" + "icon" = "string" + "color" = "string" + "comment" = "string" + } + ParamDescriptions = @{ + "name" = "string (Transaction category name)" + "type" = "integer (Transaction category type, 1: Income, 2: Expense, 3: Transfer)" + "parentId" = "string (Parent transaction category ID, 0 for primary category)" + "icon" = "string (Transaction category icon ID)" + "color" = "string (Transaction category icon color, hex color code RRGGBB)" + "comment" = "string (Transaction category description)" + } + ResponseStructure = @( + "{" + " `"id`": `"string (Transaction category ID)`"," + " `"name`": `"string (Transaction category name)`"," + " `"parentId`": `"string (Parent transaction category ID)`"," + " `"type`": `"integer (Transaction category type)`"," + " `"icon`": `"string (Transaction category icon ID)`"," + " `"color`": `"string (Transaction category icon color)`"," + " `"comment`": `"string (Transaction category description)`"," + " `"displayOrder`": `"integer (The display order of the transaction category)`"," + " `"hidden`": `"boolean (Whether the transaction category is hidden)`"," + " `"subCategories`": [`"each sub-category object like a transaction category object`"]" + "}" + ) + } + @{ + Name = "transaction-tags-list" + Description = "Get all transaction tags list" + Method = "GET" + Path = "transaction/tags/list.json" + RequiresTimezone = $false + RequiredParams = @() + OptionalParams = @() + ParamTypes = @{} + ParamDescriptions = @{} + ResponseStructure = @( + "[" + " {" + " `"id`": `"string (Transaction tag ID)`"," + " `"name`": `"string (Transaction tag name)`"," + " `"groupId`": `"string (Transaction tag group ID)`"," + " `"displayOrder`": `"integer (The display order of the transaction tag)`"," + " `"hidden`": `"boolean (Whether the transaction tag is hidden)`"" + " }" + "]" + ) + } + @{ + Name = "transaction-tags-add" + Description = "Add transaction tag" + Method = "POST" + Path = "transaction/tags/add.json" + RequiresTimezone = $false + RequiredParams = @("name") + OptionalParams = @("groupId") + ParamTypes = @{ + "name" = "string" + "groupId" = "string" + } + ParamDescriptions = @{ + "name" = "string (Transaction tag name)" + "groupId" = "string (Transaction tag group ID, 0 means default group)" + } + ResponseStructure = @( + "{" + " `"id`": `"string (Transaction tag ID)`"," + " `"name`": `"string`"," + " ..." + "}" + ) + } + @{ + Name = "transactions-list" + Description = "Get transactions list" + Method = "GET" + Path = "transactions/list.json" + RequiresTimezone = $true + RequiredParams = @("count") + OptionalParams = @("type", "category_ids", "account_ids", "tag_filter", "amount_filter", "keyword", "max_time", "min_time", "page", "with_count", "with_pictures", "trim_account", "trim_category", "trim_tag") + ParamTypes = @{ + "count" = "integer" + "type" = "integer" + "category_ids" = "string" + "account_ids" = "string" + "tag_filter" = "string" + "amount_filter" = "string" + "keyword" = "string" + "max_time" = "integer" + "min_time" = "integer" + "page" = "integer" + "with_count" = "boolean" + "with_pictures" = "boolean" + "trim_account" = "boolean" + "trim_category" = "boolean" + "trim_tag" = "boolean" + } + ParamDescriptions = @{ + "count" = "integer (The count of transactions per page, maximum is 50)" + "type" = "integer (Filter transaction by type, 1: Balance modification, 2: Income, 3: Expense, 4: Transfer)" + "category_ids" = "string (Filter by category IDs, separated by comma)" + "account_ids" = "string (Filter by account IDs, separated by comma)" + "tag_filter" = "string (Filter by tags)" + "amount_filter" = "string (Filter by amount)" + "keyword" = "string (Filter by keyword)" + "max_time" = "integer (The maximum time sequence ID, Set to 0 for latest)" + "min_time" = "integer (The minimum time sequence ID)" + "page" = "integer (Specified page integer)" + "with_count" = "boolean (Whether to get total count)" + "with_pictures" = "boolean (Whether to get picture IDs)" + "trim_account" = "boolean (Whether to get account ID only)" + "trim_category" = "boolean (Whether to get category ID only)" + "trim_tag" = "boolean (Whether to get tag IDs only)" + } + ResponseStructure = @( + "{" + " `"items`": [" + " {" + " `"id`": `"string (Transaction ID)`"," + " `"timeSequenceId`": `"string (Transaction time sequence ID)`"," + " `"type`": `"integer (Transaction type)`"," + " `"categoryId`": `"string (Transaction category ID)`"," + " `"category`": `"object (Transaction category object)`"," + " `"time`": `"integer (Transaction unix time)`"," + " `"utcOffset`": `"integer (Transaction time zone offset minutes)`"," + " `"sourceAccountId`": `"string (Source account ID)`"," + " `"sourceAccount`": `"object (Source account object)`"," + " `"destinationAccountId`": `"string (Destination account ID)`"," + " `"destinationAccount`": `"object (Destination account object)`"," + " `"sourceAmount`": `"integer (Source amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)`"," + " `"destinationAmount`": `"integer (Destination amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)`"," + " `"hideAmount`": `"boolean (Whether to hide the amount)`"," + " `"tagIds`": [`"each string representing a transaction tag ID`"]," + " `"tags`": [`"each object representing a transaction tag object`"]," + " `"pictures`": [`"each object representing a transaction picture object`"]," + " `"comment`": `"string (Transaction description)`"," + " `"geoLocation`": `"object (Transaction geographic location)`"," + " `"editable`": `"boolean (Whether the transaction is editable)`"" + " }" + " ]," + " `"nextTimeSequenceId`": `"integer (The next cursor ``max_time`` parameter when requesting older data)`"," + " `"totalCount`": `"integer (The total count of transactions)`"" + "}" + ) + } + @{ + Name = "transactions-list-all" + Description = "Get all transactions list" + Method = "GET" + Path = "transactions/list/all.json" + RequiresTimezone = $true + RequiredParams = @() + OptionalParams = @("type", "category_ids", "account_ids", "tag_filter", "amount_filter", "keyword", "start_time", "end_time", "with_pictures", "trim_account", "trim_category", "trim_tag") + ParamTypes = @{ + "type" = "integer" + "category_ids" = "string" + "account_ids" = "string" + "tag_filter" = "string" + "amount_filter" = "string" + "keyword" = "string" + "start_time" = "integer" + "end_time" = "integer" + "with_pictures" = "boolean" + "trim_account" = "boolean" + "trim_category" = "boolean" + "trim_tag" = "boolean" + } + ParamDescriptions = @{ + "type" = "integer (Filter transaction by type, 1: Balance modification, 2: Income, 3: Expense, 4: Transfer)" + "category_ids" = "string (Filter by category IDs, separated by comma)" + "account_ids" = "string (Filter by account IDs, separated by comma)" + "tag_filter" = "string (Filter by tags)" + "amount_filter" = "string (Filter by amount)" + "keyword" = "string (Filter by keyword)" + "start_time" = "integer (Transaction list start unix time)" + "end_time" = "integer (Transaction list end unix time)" + "with_pictures" = "boolean (Whether to get picture IDs)" + "trim_account" = "boolean (Whether to get account ID only)" + "trim_category" = "boolean (Whether to get category ID only)" + "trim_tag" = "boolean (Whether to get tag IDs only)" + } + ResponseStructure = @( + "[" + " {" + " `"id`": `"string (Transaction ID)`"," + " `"timeSequenceId`": `"string (Transaction time sequence ID)`"," + " `"type`": `"integer (Transaction type)`"," + " `"categoryId`": `"string (Transaction category ID)`"," + " `"category`": `"object (Transaction category object)`"," + " `"time`": `"integer (Transaction unix time)`"," + " `"utcOffset`": `"integer (Transaction time zone offset minutes)`"," + " `"sourceAccountId`": `"string (Source account ID)`"," + " `"sourceAccount`": `"object (Source account object)`"," + " `"destinationAccountId`": `"string (Destination account ID)`"," + " `"destinationAccount`": `"object (Destination account object)`"," + " `"sourceAmount`": `"integer (Source amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)`"," + " `"destinationAmount`": `"integer (Destination amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)`"," + " `"hideAmount`": `"boolean (Whether to hide the amount)`"," + " `"tagIds`": [`"each string representing a transaction tag ID`"]," + " `"tags`": [`"each object representing a transaction tag object`"]," + " `"pictures`": [`"each object representing a transaction picture object`"]," + " `"comment`": `"string (Transaction description)`"," + " `"geoLocation`": `"object (Transaction geographic location)`"," + " `"editable`": `"boolean (Whether the transaction is editable)`"" + " }" + "]" + ) + } + @{ + Name = "transactions-add" + Description = "Add transaction" + Method = "POST" + Path = "transactions/add.json" + RequiresTimezone = $true + RequiredParams = @("type", "categoryId", "time", "utcOffset", "sourceAccountId", "sourceAmount") + OptionalParams = @("destinationAccountId", "destinationAmount", "hideAmount", "tagIds", "pictureIds", "comment", "geoLocation") + ParamTypes = @{ + "type" = "integer" + "categoryId" = "string" + "time" = "integer" + "utcOffset" = "integer" + "sourceAccountId" = "string" + "sourceAmount" = "integer" + "destinationAccountId" = "string" + "destinationAmount" = "integer" + "hideAmount" = "boolean" + "tagIds" = "string_array" + "pictureIds" = "string_array" + "comment" = "string" + "geoLocation" = "geo_location" + } + ParamDescriptions = @{ + "type" = "integer (Transaction type, 1: Balance Modification, 2: Income, 3: Expense, 4: Transfer)" + "categoryId" = "string (Transaction category ID)" + "time" = "integer (Transaction unix time)" + "utcOffset" = "integer (Transaction time zone offset minutes)" + "sourceAccountId" = "string (Source account ID)" + "sourceAmount" = "integer (Source amount, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`")" + "destinationAccountId" = "string (Destination account ID)" + "destinationAmount" = "integer (Destination amount, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`")" + "hideAmount" = "boolean (Whether to hide amount)" + "tagIds" = "string (Transaction tag IDs, separated by comma, e.g. `"tagid1,tagid2`")" + "pictureIds" = "string (Transaction picture IDs, separated by comma, e.g. `"picid1,picid2`")" + "comment" = "string (Transaction description)" + "geoLocation" = "string (Transaction geographic location, format: longitude,latitude, e.g. `"116.33,39.93`")" + } + ResponseStructure = @( + "{" + " `"id`": `"string (Transaction ID)`"," + " `"timeSequenceId`": `"string (Transaction time sequence ID)`"," + " `"type`": `"integer (Transaction type)`"," + " `"categoryId`": `"string (Transaction category ID)`"," + " `"category`": `"object (Transaction category object)`"," + " `"time`": `"integer (Transaction unix time)`"," + " `"utcOffset`": `"integer (Transaction time zone offset minutes)`"," + " `"sourceAccountId`": `"string (Source account ID)`"," + " `"sourceAccount`": `"object (Source account object)`"," + " `"destinationAccountId`": `"string (Destination account ID)`"," + " `"destinationAccount`": `"object (Destination account object)`"," + " `"sourceAmount`": `"integer (Source amount)`"," + " `"destinationAmount`": `"integer (Destination amount)`"," + " `"hideAmount`": `"boolean (Whether to hide the amount)`"," + " `"tagIds`": [`"each string representing a transaction tag ID`"]," + " `"tags`": [`"each object representing a transaction tag object`"]," + " `"pictures`": [`"each object representing a transaction picture object`"]," + " `"comment`": `"string (Transaction description)`"," + " `"geoLocation`": `"object (Transaction geographic location)`"," + " `"editable`": `"boolean (Whether the transaction is editable)`"" + "}" + ) + } +) + +# Reference: https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/windowsZones.xml +$TIMEZONE_IANA_NAMES = @{ + "Dateline Standard Time" = "Etc/GMT+12" + "UTC-11" = "Etc/GMT+11" + "Aleutian Standard Time" = "America/Adak" + "Hawaiian Standard Time" = "Pacific/Honolulu" + "Marquesas Standard Time" = "Pacific/Marquesas" + "Alaskan Standard Time" = "America/Anchorage" + "UTC-09" = "Etc/GMT+9" + "Pacific Standard Time (Mexico)" = "America/Tijuana" + "UTC-08" = "Etc/GMT+8" + "Pacific Standard Time" = "America/Los_Angeles" + "US Mountain Standard Time" = "America/Phoenix" + "Mountain Standard Time (Mexico)" = "America/Chihuahua" + "Mountain Standard Time" = "America/Denver" + "Yukon Standard Time" = "America/Whitehorse" + "Central America Standard Time" = "America/Guatemala" + "Central Standard Time" = "America/Chicago" + "Easter Island Standard Time" = "Pacific/Easter" + "Central Standard Time (Mexico)" = "America/Mexico_City" + "Canada Central Standard Time" = "America/Regina" + "SA Pacific Standard Time" = "America/Bogota" + "Eastern Standard Time (Mexico)" = "America/Cancun" + "Eastern Standard Time" = "America/New_York" + "Haiti Standard Time" = "America/Port-au-Prince" + "Cuba Standard Time" = "America/Havana" + "US Eastern Standard Time" = "America/Indianapolis" + "Turks And Caicos Standard Time" = "America/Grand_Turk" + "Paraguay Standard Time" = "America/Asuncion" + "Atlantic Standard Time" = "America/Halifax" + "Venezuela Standard Time" = "America/Caracas" + "Central Brazilian Standard Time" = "America/Cuiaba" + "SA Western Standard Time" = "America/La_Paz" + "Pacific SA Standard Time" = "America/Santiago" + "Newfoundland Standard Time" = "America/St_Johns" + "Tocantins Standard Time" = "America/Araguaina" + "E. South America Standard Time" = "America/Sao_Paulo" + "SA Eastern Standard Time" = "America/Cayenne" + "Argentina Standard Time" = "America/Buenos_Aires" + "Greenland Standard Time" = "America/Godthab" + "Montevideo Standard Time" = "America/Montevideo" + "Magallanes Standard Time" = "America/Punta_Arenas" + "Saint Pierre Standard Time" = "America/Miquelon" + "Bahia Standard Time" = "America/Bahia" + "UTC-02" = "Etc/GMT+2" + "Azores Standard Time" = "Atlantic/Azores" + "Cape Verde Standard Time" = "Atlantic/Cape_Verde" + "UTC" = "Etc/UTC" + "GMT Standard Time" = "Europe/London" + "Greenwich Standard Time" = "Atlantic/Reykjavik" + "Sao Tome Standard Time" = "Africa/Sao_Tome" + "Morocco Standard Time" = "Africa/Casablanca" + "W. Europe Standard Time" = "Europe/Berlin" + "Central Europe Standard Time" = "Europe/Budapest" + "Romance Standard Time" = "Europe/Paris" + "Central European Standard Time" = "Europe/Warsaw" + "W. Central Africa Standard Time" = "Africa/Lagos" + "Jordan Standard Time" = "Asia/Amman" + "GTB Standard Time" = "Europe/Bucharest" + "Middle East Standard Time" = "Asia/Beirut" + "Egypt Standard Time" = "Africa/Cairo" + "E. Europe Standard Time" = "Europe/Chisinau" + "Syria Standard Time" = "Asia/Damascus" + "West Bank Standard Time" = "Asia/Hebron" + "South Africa Standard Time" = "Africa/Johannesburg" + "FLE Standard Time" = "Europe/Kiev" + "Israel Standard Time" = "Asia/Jerusalem" + "South Sudan Standard Time" = "Africa/Juba" + "Kaliningrad Standard Time" = "Europe/Kaliningrad" + "Sudan Standard Time" = "Africa/Khartoum" + "Libya Standard Time" = "Africa/Tripoli" + "Namibia Standard Time" = "Africa/Windhoek" + "Arabic Standard Time" = "Asia/Baghdad" + "Turkey Standard Time" = "Europe/Istanbul" + "Arab Standard Time" = "Asia/Riyadh" + "Belarus Standard Time" = "Europe/Minsk" + "Russian Standard Time" = "Europe/Moscow" + "E. Africa Standard Time" = "Africa/Nairobi" + "Iran Standard Time" = "Asia/Tehran" + "Arabian Standard Time" = "Asia/Dubai" + "Astrakhan Standard Time" = "Europe/Astrakhan" + "Azerbaijan Standard Time" = "Asia/Baku" + "Russia Time Zone 3" = "Europe/Samara" + "Mauritius Standard Time" = "Indian/Mauritius" + "Saratov Standard Time" = "Europe/Saratov" + "Georgian Standard Time" = "Asia/Tbilisi" + "Volgograd Standard Time" = "Europe/Volgograd" + "Caucasus Standard Time" = "Asia/Yerevan" + "Afghanistan Standard Time" = "Asia/Kabul" + "West Asia Standard Time" = "Asia/Tashkent" + "Ekaterinburg Standard Time" = "Asia/Yekaterinburg" + "Pakistan Standard Time" = "Asia/Karachi" + "Qyzylorda Standard Time" = "Asia/Qyzylorda" + "India Standard Time" = "Asia/Calcutta" + "Sri Lanka Standard Time" = "Asia/Colombo" + "Nepal Standard Time" = "Asia/Katmandu" + "Central Asia Standard Time" = "Asia/Bishkek" + "Bangladesh Standard Time" = "Asia/Dhaka" + "Omsk Standard Time" = "Asia/Omsk" + "Myanmar Standard Time" = "Asia/Rangoon" + "SE Asia Standard Time" = "Asia/Bangkok" + "Altai Standard Time" = "Asia/Barnaul" + "W. Mongolia Standard Time" = "Asia/Hovd" + "North Asia Standard Time" = "Asia/Krasnoyarsk" + "N. Central Asia Standard Time" = "Asia/Novosibirsk" + "Tomsk Standard Time" = "Asia/Tomsk" + "China Standard Time" = "Asia/Shanghai" + "North Asia East Standard Time" = "Asia/Irkutsk" + "Singapore Standard Time" = "Asia/Singapore" + "W. Australia Standard Time" = "Australia/Perth" + "Taipei Standard Time" = "Asia/Taipei" + "Ulaanbaatar Standard Time" = "Asia/Ulaanbaatar" + "Aus Central W. Standard Time" = "Australia/Eucla" + "Transbaikal Standard Time" = "Asia/Chita" + "Tokyo Standard Time" = "Asia/Tokyo" + "North Korea Standard Time" = "Asia/Pyongyang" + "Korea Standard Time" = "Asia/Seoul" + "Yakutsk Standard Time" = "Asia/Yakutsk" + "Cen. Australia Standard Time" = "Australia/Adelaide" + "AUS Central Standard Time" = "Australia/Darwin" + "E. Australia Standard Time" = "Australia/Brisbane" + "AUS Eastern Standard Time" = "Australia/Sydney" + "West Pacific Standard Time" = "Pacific/Port_Moresby" + "Tasmania Standard Time" = "Australia/Hobart" + "Vladivostok Standard Time" = "Asia/Vladivostok" + "Lord Howe Standard Time" = "Australia/Lord_Howe" + "Bougainville Standard Time" = "Pacific/Bougainville" + "Russia Time Zone 10" = "Asia/Srednekolymsk" + "Magadan Standard Time" = "Asia/Magadan" + "Norfolk Standard Time" = "Pacific/Norfolk" + "Sakhalin Standard Time" = "Asia/Sakhalin" + "Central Pacific Standard Time" = "Pacific/Guadalcanal" + "Russia Time Zone 11" = "Asia/Kamchatka" + "New Zealand Standard Time" = "Pacific/Auckland" + "UTC+12" = "Etc/GMT-12" + "Fiji Standard Time" = "Pacific/Fiji" + "Chatham Islands Standard Time" = "Pacific/Chatham" + "UTC+13" = "Etc/GMT-13" + "Tonga Standard Time" = "Pacific/Tongatapu" + "Samoa Standard Time" = "Pacific/Apia" + "Line Islands Standard Time" = "Pacific/Kiritimati" +} + +function Write-Red($msg) { + Write-Host $msg -ForegroundColor Red +} + +function Write-Yellow($msg) { + Write-Host $msg -ForegroundColor Yellow +} + +function Url-Encode { + param([string]$text) + return [System.Uri]::EscapeDataString($text) +} + +function Format-Json { + # Reference: https://jonathancrozier.com/blog/formatting-json-with-proper-indentation-using-powershell + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [String]$Json, + + [ValidateRange(1, 1024)] + [Int]$Indentation = 2 + ) + + $indentationLevel = 0 + $insideString = $false + $previousCharacterWasEscape = $false + $stringBuilder = New-Object System.Text.StringBuilder + $characters = $Json.ToCharArray() + + for ($i = 0; $i -lt $characters.Length; $i++) { + $character = $characters[$i] + + if ($insideString) { + [void]$stringBuilder.Append($character) + + if ($previousCharacterWasEscape) { + $previousCharacterWasEscape = $false + } elseif ($character -eq '\') { + if ($i + 1 -lt $characters.Length) { + $nextCharacter = $characters[$i + 1] + + if ($nextCharacter -in @('"', '\', '/', 'b', 'f', 'n', 'r', 't', 'u')) { + $previousCharacterWasEscape = $true + } + } + } elseif ($character -eq '"') { + $insideString = $false + } + } else { + switch ($character) { + '"'{ + $insideString = $true + [void]$stringBuilder.Append($character) + } + '{'{ + [void]$stringBuilder.Append($character) + + if ($i + 1 -lt $characters.Length -and $characters[$i + 1] -eq '}') { + [void]$stringBuilder.Append('}') + $i++ + continue + } + + $indentationLevel++ + [void]$stringBuilder.Append("`n" + (' ' * ($indentationLevel * $Indentation))) + } + '['{ + [void]$stringBuilder.Append($character) + + if ($i + 1 -lt $characters.Length -and $characters[$i + 1] -eq ']') { + [void]$stringBuilder.Append(']') + $i++ + continue + } + + $indentationLevel++ + [void]$stringBuilder.Append("`n" + (' ' * ($indentationLevel * $Indentation))) + } + '}'{ + $indentationLevel-- + [void]$stringBuilder.Append("`n" + (' ' * ($indentationLevel * $Indentation)) + $character) + } + ']'{ + $indentationLevel-- + [void]$stringBuilder.Append("`n" + (' ' * ($indentationLevel * $Indentation)) + $character) + } + ','{ + [void]$stringBuilder.Append($character) + [void]$stringBuilder.Append("`n" + (' ' * ($indentationLevel * $Indentation))) + } + ':'{ + [void]$stringBuilder.Append(": ") + } + default{ + if (-not [char]::IsWhiteSpace($character)) { + [void]$stringBuilder.Append($character) + } + } + } + } + } + + return $stringBuilder.ToString() +} + +function Get-SystemTimezoneName { + try { + $tz = [System.TimeZoneInfo]::Local + $windowsId = $tz.Id + + if ($windowsId) { + try { + $ianaId = $null + $result = [System.TimeZoneInfo]::TryConvertWindowsIdToIanaId($windowsId, [ref]$ianaId) + if ($result -and $ianaId) { + return $ianaId + } + } catch { + # Do Nothing + } + + # Fallback mapping for common Windows timezone IDs + if ($TIMEZONE_IANA_NAMES.ContainsKey($windowsId)) { + return $TIMEZONE_IANA_NAMES[$windowsId] + } + } + } catch { + # Do Nothing + } + return "Asia/Shanghai" +} + +function Get-SystemTimezoneOffset { + try { + $offset = [System.TimeZoneInfo]::Local.BaseUtcOffset + return [int]$offset.TotalMinutes + } catch { + # Do Nothing + } + return 480 +} + +function Get-ApiConfig { + param([string]$apiName) + + foreach ($config in $API_CONFIGS) { + if ($config.Name -eq $apiName) { + return $config + } + } + + return $null +} + +function Show-Help { + $tzName = Get-SystemTimezoneName + $tzOffset = Get-SystemTimezoneOffset + + Write-Host "ezBookkeeping API Tools" + Write-Host "" + Write-Host "A command-line tool for calling ezBookkeeping APIs" + Write-Host "" + Write-Host "Usage:" + Write-Host " ebktools.ps1 [-tzName ] [-tzOffset ] [command-options]" + Write-Host "" + Write-Host "Global Options:" + Write-Host " -tzName The IANA timezone name, for example, for Beijing Time it is 'Asia/Shanghai'." + Write-Host " -tzOffset The offset in minutes of the current timezone from UTC. For example, for Beijing Time which is UTC+8, the value is '480'. If both are provided, '-tzName' takes precedence." + Write-Host "" + Write-Host "Environment Variables (Required):" + Write-Host " EBKTOOL_SERVER_BASEURL ezBookkeeping server base URL (e.g., http://localhost:8080)" + Write-Host " EBKTOOL_TOKEN ezBookkeeping API token" + Write-Host "" + Write-Host "Commands:" + Write-Host " list List all available API commands" + Write-Host " help Show help for a specific API command" + Write-Host " Execute an API command" + Write-Host "" + Write-Host "Examples:" + Write-Host " # Set environment variables" + Write-Host " `$env:EBKTOOL_SERVER_BASEURL = 'http://localhost:8080'" + Write-Host " `$env:EBKTOOL_TOKEN = 'YOUR_TOKEN'" + Write-Host "" + Write-Host " # List all available commands" + Write-Host " ebktools.ps1 list" + Write-Host "" + Write-Host " # Show help for a specific command" + Write-Host " ebktools.ps1 help transactions-add" + Write-Host "" + Write-Host " # Call accounts-list API" + Write-Host " ebktools.ps1 accounts-list" + Write-Host "" + Write-Host " # Call API with timezone name" + Write-Host " ebktools.ps1 -tzName $tzName transactions-list -count 10" + Write-Host "" + Write-Host " # Call API with timezone offset" + Write-Host " ebktools.ps1 -tzOffset $tzOffset transactions-list -count 10" +} + +function Show-CommandList { + Write-Host "Available API Commands:" + Write-Host "" + + foreach ($config in $API_CONFIGS) { + $name = $config.Name.PadRight(31) + Write-Host " $name$($config.Description)" + } + + Write-Host "" + Write-Host "Use 'ebktools.ps1 help ' to see detailed information about a API command." +} + +function Show-CommandHelp { + param([string]$commandName) + + $config = Get-ApiConfig $commandName + + if (-not $config) { + Write-Red "Error: Unknown command '$commandName'" + Write-Host "" + Write-Host "Use 'ebktools.ps1 list' to see all available commands." + exit 1 + } + + Write-Host "Command: $($config.Name)" + Write-Host "Description: $($config.Description)" + Write-Host "Method: $($config.Method)" + Write-Host "Path: $($config.Path)" + Write-Host "" + + if ($config.RequiredParams.Count -gt 0) { + Write-Host "Required Parameters:" + foreach ($param in $config.RequiredParams) { + $desc = $config.ParamDescriptions[$param] + Write-Host " -$($param.PadRight(26)) $desc" + } + Write-Host "" + } + + if ($config.OptionalParams.Count -gt 0) { + Write-Host "Optional Parameters:" + foreach ($param in $config.OptionalParams) { + $desc = $config.ParamDescriptions[$param] + Write-Host " -$($param.PadRight(26)) $desc" + } + Write-Host "" + } + + if ($config.ResponseStructure) { + Write-Host "Response Structure:" + foreach ($line in $config.ResponseStructure) { + Write-Host " $line" + } + Write-Host "" + } + + Write-Host "Example:" + if ($config.RequiresTimezone) { + $tzName = Get-SystemTimezoneName + Write-Host " ebktools.ps1 -tzName $tzName $($config.Name)" + } else { + Write-Host " ebktools.ps1 $($config.Name)" + } +} + +function Parse-CommandArgs { + param( + [string[]]$commandArgs, + [hashtable]$paramTypes + ) + + $params = @{} + $i = 0 + + while ($i -lt $commandArgs.Count) { + $arg = $commandArgs[$i] + + if ($arg.StartsWith("-")) { + $paramName = $arg.Substring(1) + + if ($i + 1 -lt $commandArgs.Count -and -not $commandArgs[$i + 1].StartsWith("-")) { + $paramType = "string" + $paramValue = $commandArgs[$i + 1] + + if ($paramTypes -and $paramTypes.ContainsKey($paramName)) { + $paramType = $paramTypes[$paramName] + } + + try { + switch ($paramType) { + "integer" { + $numValue = [long]::Parse($paramValue) + $params[$paramName] = $numValue + } + "boolean" { + if ($paramValue -match "^(true|false|1|0)$") { + $params[$paramName] = ($paramValue -eq "true" -or $paramValue -eq "1") + } else { + Write-Red "Error: Parameter '-$paramName' must be a boolean value (true/false or 1/0)" + exit 1 + } + } + "string_array" { + $arrayValues = $paramValue.Split(",") + $params[$paramName] = $arrayValues + } + "geo_location" { + $coords = $paramValue.Split(",") + if ($coords.Count -ne 2) { + Write-Red "Error: Parameter '-$paramName' must be in format 'longitude,latitude'" + exit 1 + } + + $longitude = [double]::Parse($coords[0]) + $latitude = [double]::Parse($coords[1]) + + $geoLocation = @{ + latitude = $latitude + longitude = $longitude + } + + $params[$paramName] = $geoLocation + } + default { + $params[$paramName] = $paramValue + } + } + } catch { + Write-Red "Error: Parameter '-$paramName' has invalid $paramType value: '$paramValue'" + exit 1 + } + + $i += 2 + } else { + Write-Red "Error: Parameter '-$paramName' requires a value" + exit 1 + } + } else { + Write-Red "Error: Invalid parameter format: '$arg'" + exit 1 + } + } + + return $params +} + +function Invoke-Api { + param( + [string]$commandName, + [string[]]$commandArgs + ) + + $config = Get-ApiConfig $commandName + + if (-not $config) { + Write-Red "Error: Unknown command '$commandName'" + Write-Host "" + Write-Host "Use 'ebktools.ps1 list' to see all available commands." + exit 1 + } + + $serverBaseUrl = $env:EBKTOOL_SERVER_BASEURL + $authToken = $env:EBKTOOL_TOKEN + + if (-not $serverBaseUrl) { + Write-Red "Error: Environment variable 'EBKTOOL_SERVER_BASEURL' is not set." + Write-Host "Please set it to your API server base URL (e.g., http://localhost:8080)" + exit 1 + } + + if (-not $authToken) { + Write-Red "Error: Environment variable 'EBKTOOL_TOKEN' is not set." + Write-Host "Please set it to your authentication token." + exit 1 + } + + if ($config.RequiresTimezone -and -not $script:tzName -and -not $script:tzOffset) { + $tzName = Get-SystemTimezoneName + $tzOffset = Get-SystemTimezoneOffset + Write-Red "Error: Command '$commandName' requires timezone information." + Write-Host "Please provide either '-tzName' or '-tzOffset' parameter." + Write-Host "" + Write-Host "Examples:" + Write-Host " ebktools.ps1 -tzName $tzName $commandName ..." + Write-Host " ebktools.ps1 -tzOffset $tzOffset $commandName ..." + exit 1 + } + + $paramTypes = @{} + if ($config.ParamTypes) { + $paramTypes = $config.ParamTypes + } + + $params = Parse-CommandArgs -commandArgs $commandArgs -paramTypes $paramTypes + + foreach ($requiredParam in $config.RequiredParams) { + if (-not $params.ContainsKey($requiredParam)) { + Write-Red "Error: Required parameter '-$requiredParam' is missing" + exit 1 + } + } + + if ($serverBaseUrl.EndsWith("/")) { + $serverBaseUrl = $serverBaseUrl.Substring(0, $serverBaseUrl.Length - 1) + } + + $url = "$serverBaseUrl/api/v1/$($config.Path)" + + try { + $headers = @{ + "Authorization" = "Bearer $authToken" + } + + if ($script:tzName) { + $headers["X-Timezone-Name"] = $script:tzName + } elseif ($script:tzOffset) { + $headers["X-Timezone-Offset"] = $script:tzOffset + } + + if ($config.Method -eq "POST") { + $headers["Content-Type"] = "application/json" + + Write-Yellow "Calling API: $($config.Method) $url" + Write-Host "" + + if ($params.Count -gt 0) { + $body = ConvertTo-Json -Depth 10 $params + $response = Invoke-WebRequest -Uri $url -Method POST -Headers $headers -Body $body -ErrorAction Stop -UseBasicParsing + } else { + $response = Invoke-WebRequest -Uri $url -Method POST -Headers $headers -ErrorAction Stop -UseBasicParsing + } + + $response = ConvertFrom-Json $response.Content + } else { + if ($params.Count -gt 0) { + $queryString = ($params.GetEnumerator() | ForEach-Object { "$($_.Key)=$(Url-Encode $_.Value)" }) -join "&" + $url = "${url}?$queryString" + } + + Write-Yellow "Calling API: $($config.Method) $url" + Write-Host "" + + $response = Invoke-WebRequest -Uri $url -Method $config.Method -Headers $headers -ErrorAction Stop -UseBasicParsing + $response = ConvertFrom-Json $response.Content + } + + if ($response.PSObject.Properties.Name -contains "success") { + if ($response.success -eq $true) { + Write-Host "Response Result:" + if ($response.PSObject.Properties.Name -contains "result") { + $jsonOutput = ConvertTo-Json -Depth 10 -Compress $response.result | Format-Json + Write-Host $jsonOutput + } else { + Write-Host "Success: true (No result data)" + } + } else { + Write-Host "Raw Response:" + $jsonOutput = ConvertTo-Json -Depth 10 -Compress $response | Format-Json + Write-Host $jsonOutput + } + } else { + Write-Host "Raw Response:" + $jsonOutput = ConvertTo-Json -Depth 10 -Compress $response | Format-Json + Write-Host $jsonOutput + } + } catch { + if ($_.ErrorDetails.Message) { + Write-Host "Raw Response:" + try { + $errorJson = ConvertFrom-Json $_.ErrorDetails.Message + $formattedJson = ConvertTo-Json -Depth 10 -Compress $errorJson | Format-Json + Write-Host $formattedJson + } catch { + Write-Host $_.ErrorDetails.Message + } + } else { + $exceptionMessage = $_.Exception.Message + Write-Red "Error: API call failed ($exceptionMessage)" + exit 1 + } + } +} + +function Main { + if ($Command -eq "list") { + Show-CommandList + exit 0 + } + + if ($Command -eq "help") { + if ($CommandArgs.Count -eq 0) { + Show-Help + } else { + Show-CommandHelp $CommandArgs[0] + } + exit 0 + } + + if (-not $Command) { + Show-Help + exit 0 + } + + Invoke-Api -commandName $Command -commandArgs $CommandArgs +} + +Main diff --git a/scripts/ebktools.sh b/scripts/ebktools.sh new file mode 100755 index 00000000..ad48eddf --- /dev/null +++ b/scripts/ebktools.sh @@ -0,0 +1,949 @@ +#!/usr/bin/env sh + +# ezBookkeeping API Tools +# A command-line tool for calling ezBookkeeping APIs + +# API Configuration Structure +API_CONFIGS='[ + { + "Name": "tokens-list", + "Description": "Get available sessions information", + "Method": "GET", + "Path": "tokens/list.json", + "RequiresTimezone": false, + "RequiredParams": [], + "OptionalParams": [], + "ParamTypes": {}, + "ParamDescriptions": {}, + "ResponseStructure": [ + "[", + " {", + " \"tokenId\": \"string (Token ID)\",", + " \"tokenType\": \"integer (Token type, 1: Normal Token, 5: MCP Token, 8: API Token)\",", + " \"userAgent\": \"string (The User Agent when the session created)\",", + " \"lastSeen\": \"integer (Last refresh unix time of the session)\",", + " \"isCurrent\": \"boolean (Whether the session is current)\"", + " }", + "]" + ] + }, + { + "Name": "tokens-revoke", + "Description": "Revoke token", + "Method": "POST", + "Path": "tokens/revoke.json", + "RequiresTimezone": false, + "RequiredParams": ["tokenId"], + "OptionalParams": [], + "ParamTypes": { + "tokenId": "string" + }, + "ParamDescriptions": { + "tokenId": "string (Token ID)" + }, + "ResponseStructure": [ + "boolean (Whether the token is revoked successfully)" + ] + }, + { + "Name": "accounts-list", + "Description": "Get all accounts list", + "Method": "GET", + "Path": "accounts/list.json", + "RequiresTimezone": false, + "RequiredParams": [], + "OptionalParams": [], + "ParamTypes": {}, + "ParamDescriptions": {}, + "ResponseStructure": [ + "[", + " {", + " \"id\": \"string (Account ID)\",", + " \"name\": \"string (Account name)\",", + " \"parentId\": \"string (Parent account ID)\",", + " \"category\": \"integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)\",", + " \"type\": \"integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)\",", + " \"icon\": \"string (Account icon ID)\",", + " \"color\": \"string (Account icon color, hex color code RRGGBB)\",", + " \"currency\": \"string (Account currency code)\",", + " \"balance\": \"integer (Account balance, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\")\",", + " \"comment\": \"string (Account description)\",", + " \"creditCardStatementDate\": \"integer (The statement date of the credit card account)\",", + " \"displayOrder\": \"integer (The display order of the account)\",", + " \"isAsset\": \"boolean (Whether the account is an asset account)\",", + " \"isLiability\": \"boolean (Whether the account is a liability account)\",", + " \"hidden\": \"boolean (Whether the account is hidden)\",", + " \"subAccounts\": [\"each sub-account object like an account object\"]", + " }", + "]" + ] + }, + { + "Name": "accounts-add", + "Description": "Add account", + "Method": "POST", + "Path": "accounts/add.json", + "RequiresTimezone": true, + "RequiredParams": ["name", "category", "type", "icon", "color", "currency"], + "OptionalParams": ["balance", "balanceTime", "comment", "creditCardStatementDate"], + "ParamTypes": { + "name": "string", + "category": "integer", + "type": "integer", + "icon": "string", + "color": "string", + "currency": "string", + "balance": "integer", + "balanceTime": "integer", + "comment": "string", + "creditCardStatementDate": "integer" + }, + "ParamDescriptions": { + "name": "string (Account name)", + "category": "integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)", + "type": "integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)", + "icon": "string (Account icon ID)", + "color": "string (Account icon color, hex color code RRGGBB)", + "currency": "string (Account currency code, ISO 4217 code, \\"---\\" for the parent account)", + "balance": "integer (Account balance, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\". Liability account should set to negative amount)", + "balanceTime": "integer (The unix time when the account balance is the set value. This field is required when balance is set)", + "comment": "string (Account description)", + "creditCardStatementDate": "integer (The statement date of the credit card account)" + }, + "ResponseStructure": [ + "{", + " \"id\": \"string (Account ID)\",", + " \"name\": \"string (Account name)\",", + " \"parentId\": \"string (Parent account ID)\",", + " \"category\": \"integer (Account category)\",", + " \"type\": \"integer (Account type)\",", + " \"icon\": \"string (Account icon ID)\",", + " \"color\": \"string (Account icon color)\",", + " \"currency\": \"string (Account currency code)\",", + " \"balance\": \"integer (Account balance)\",", + " \"comment\": \"string (Account description)\",", + " \"creditCardStatementDate\": \"integer (The statement date of the credit card account)\",", + " \"displayOrder\": \"integer (The display order of the account)\",", + " \"isAsset\": \"boolean (Whether the account is an asset account)\",", + " \"isLiability\": \"boolean (Whether the account is a liability account)\",", + " \"hidden\": \"boolean (Whether the account is hidden)\",", + " \"subAccounts\": [\"every sub-account object like account object\"]", + "}" + ] + }, + { + "Name": "transaction-categories-list", + "Description": "Get all transaction categories", + "Method": "GET", + "Path": "transaction/categories/list.json", + "RequiresTimezone": false, + "RequiredParams": [], + "OptionalParams": [], + "ParamTypes": {}, + "ParamDescriptions": {}, + "ResponseStructure": [ + "{", + " \"transaction category type (1: Income, 2: Expense, 3:Transfer)\": [", + " {", + " \"id\": \"string (Transaction category ID)\",", + " \"name\": \"string (Transaction category name)\",", + " \"parentId\": \"string (Parent transaction category ID)\",", + " \"type\": \"integer (Transaction category type)\",", + " \"icon\": \"string (Transaction category icon ID)\",", + " \"color\": \"string (Transaction category icon color, hex color code RRGGBB)\",", + " \"comment\": \"string (Transaction category description)\",", + " \"displayOrder\": \"integer (The display order of the transaction category)\",", + " \"hidden\": \"boolean (Whether the transaction category is hidden)\",", + " \"subCategories\": [\"each sub-category object like a transaction category object\"]", + " }", + " ]", + "}" + ] + }, + { + "Name": "transaction-categories-add", + "Description": "Add transaction category", + "Method": "POST", + "Path": "transaction/categories/add.json", + "RequiresTimezone": false, + "RequiredParams": ["name", "type", "icon", "color"], + "OptionalParams": ["parentId", "comment"], + "ParamTypes": { + "name": "string", + "type": "integer", + "parentId": "string", + "icon": "string", + "color": "string", + "comment": "string" + }, + "ParamDescriptions": { + "name": "string (Transaction category name)", + "type": "integer (Transaction category type, 1: Income, 2: Expense, 3: Transfer)", + "parentId": "string (Parent transaction category ID, 0 for primary category)", + "icon": "string (Transaction category icon ID)", + "color": "string (Transaction category icon color, hex color code RRGGBB)", + "comment": "string (Transaction category description)" + }, + "ResponseStructure": [ + "{", + " \"id\": \"string (Transaction category ID)\",", + " \"name\": \"string (Transaction category name)\",", + " \"parentId\": \"string (Parent transaction category ID)\",", + " \"type\": \"integer (Transaction category type)\",", + " \"icon\": \"string (Transaction category icon ID)\",", + " \"color\": \"string (Transaction category icon color)\",", + " \"comment\": \"string (Transaction category description)\",", + " \"displayOrder\": \"integer (The display order of the transaction category)\",", + " \"hidden\": \"boolean (Whether the transaction category is hidden)\",", + " \"subCategories\": [\"each sub-category object like a transaction category object\"]", + "}" + ] + }, + { + "Name": "transaction-tags-list", + "Description": "Get all transaction tags list", + "Method": "GET", + "Path": "transaction/tags/list.json", + "RequiresTimezone": false, + "RequiredParams": [], + "OptionalParams": [], + "ParamTypes": {}, + "ParamDescriptions": {}, + "ResponseStructure": [ + "[", + " {", + " \"id\": \"string (Transaction tag ID)\",", + " \"name\": \"string (Transaction tag name)\",", + " \"groupId\": \"string (Transaction tag group ID)\",", + " \"displayOrder\": \"integer (The display order of the transaction tag)\",", + " \"hidden\": \"boolean (Whether the transaction tag is hidden)\"", + " }", + "]" + ] + }, + { + "Name": "transaction-tags-add", + "Description": "Add transaction tag", + "Method": "POST", + "Path": "transaction/tags/add.json", + "RequiresTimezone": false, + "RequiredParams": ["name"], + "OptionalParams": ["groupId"], + "ParamTypes": { + "name": "string", + "groupId": "string" + }, + "ParamDescriptions": { + "name": "string (Transaction tag name)", + "groupId": "string (Transaction tag group ID, 0 means default group)" + }, + "ResponseStructure": [ + "{", + " \"id\": \"string (Transaction tag ID)\",", + " \"name\": \"string\",", + " ...", + "}" + ] + }, + { + "Name": "transactions-list", + "Description": "Get transactions list", + "Method": "GET", + "Path": "transactions/list.json", + "RequiresTimezone": true, + "RequiredParams": ["count"], + "OptionalParams": ["type", "category_ids", "account_ids", "tag_filter", "amount_filter", "keyword", "max_time", "min_time", "page", "with_count", "with_pictures", "trim_account", "trim_category", "trim_tag"], + "ParamTypes": { + "count": "integer", + "type": "integer", + "category_ids": "string", + "account_ids": "string", + "tag_filter": "string", + "amount_filter": "string", + "keyword": "string", + "max_time": "integer", + "min_time": "integer", + "page": "integer", + "with_count": "boolean", + "with_pictures": "boolean", + "trim_account": "boolean", + "trim_category": "boolean", + "trim_tag": "boolean" + }, + "ParamDescriptions": { + "count": "integer (The count of transactions per page, maximum is 50)", + "type": "integer (Filter transaction by type, 1: Balance modification, 2: Income, 3: Expense, 4: Transfer)", + "category_ids": "string (Filter by category IDs, separated by comma)", + "account_ids": "string (Filter by account IDs, separated by comma)", + "tag_filter": "string (Filter by tags)", + "amount_filter": "string (Filter by amount)", + "keyword": "string (Filter by keyword)", + "max_time": "integer (The maximum time sequence ID, Set to 0 for latest)", + "min_time": "integer (The minimum time sequence ID)", + "page": "integer (Specified page integer)", + "with_count": "boolean (Whether to get total count)", + "with_pictures": "boolean (Whether to get picture IDs)", + "trim_account": "boolean (Whether to get account ID only)", + "trim_category": "boolean (Whether to get category ID only)", + "trim_tag": "boolean (Whether to get tag IDs only)" + }, + "ResponseStructure": [ + "{", + " \"items\": [", + " {", + " \"id\": \"string (Transaction ID)\",", + " \"timeSequenceId\": \"string (Transaction time sequence ID)\",", + " \"type\": \"integer (Transaction type)\",", + " \"categoryId\": \"string (Transaction category ID)\",", + " \"category\": \"object (Transaction category object)\",", + " \"time\": \"integer (Transaction unix time)\",", + " \"utcOffset\": \"integer (Transaction time zone offset minutes)\",", + " \"sourceAccountId\": \"string (Source account ID)\",", + " \"sourceAccount\": \"object (Source account object)\",", + " \"destinationAccountId\": \"string (Destination account ID)\",", + " \"destinationAccount\": \"object (Destination account object)\",", + " \"sourceAmount\": \"integer (Source amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)\",", + " \"destinationAmount\": \"integer (Destination amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)\",", + " \"hideAmount\": \"boolean (Whether to hide the amount)\",", + " \"tagIds\": [\"each string representing a transaction tag ID\"],", + " \"tags\": [\"each object representing a transaction tag object\"],", + " \"pictures\": [\"each object representing a transaction picture object\"],", + " \"comment\": \"string (Transaction description)\",", + " \"geoLocation\": \"object (Transaction geographic location)\",", + " \"editable\": \"boolean (Whether the transaction is editable)\"", + " }", + " ],", + " \"nextTimeSequenceId\": \"integer (The next cursor `max_time` parameter when requesting older data)\",", + " \"totalCount\": \"integer (The total count of transactions)\"", + "}" + ] + }, + { + "Name": "transactions-list-all", + "Description": "Get all transactions list", + "Method": "GET", + "Path": "transactions/list/all.json", + "RequiresTimezone": true, + "RequiredParams": [], + "OptionalParams": ["type", "category_ids", "account_ids", "tag_filter", "amount_filter", "keyword", "start_time", "end_time", "with_pictures", "trim_account", "trim_category", "trim_tag"], + "ParamTypes": { + "type": "integer", + "category_ids": "string", + "account_ids": "string", + "tag_filter": "string", + "amount_filter": "string", + "keyword": "string", + "start_time": "integer", + "end_time": "integer", + "with_pictures": "boolean", + "trim_account": "boolean", + "trim_category": "boolean", + "trim_tag": "boolean" + }, + "ParamDescriptions": { + "type": "integer (Filter transaction by type, 1: Balance modification, 2: Income, 3: Expense, 4: Transfer)", + "category_ids": "string (Filter by category IDs, separated by comma)", + "account_ids": "string (Filter by account IDs, separated by comma)", + "tag_filter": "string (Filter by tags)", + "amount_filter": "string (Filter by amount)", + "keyword": "string (Filter by keyword)", + "start_time": "integer (Transaction list start unix time)", + "end_time": "integer (Transaction list end unix time)", + "with_pictures": "boolean (Whether to get picture IDs)", + "trim_account": "boolean (Whether to get account ID only)", + "trim_category": "boolean (Whether to get category ID only)", + "trim_tag": "boolean (Whether to get tag IDs only)" + }, + "ResponseStructure": [ + "[", + " {", + " \"id\": \"string (Transaction ID)\",", + " \"timeSequenceId\": \"string (Transaction time sequence ID)\",", + " \"type\": \"integer (Transaction type)\",", + " \"categoryId\": \"string (Transaction category ID)\",", + " \"category\": \"object (Transaction category object)\",", + " \"time\": \"integer (Transaction unix time)\",", + " \"utcOffset\": \"integer (Transaction time zone offset minutes)\",", + " \"sourceAccountId\": \"string (Source account ID)\",", + " \"sourceAccount\": \"object (Source account object)\",", + " \"destinationAccountId\": \"string (Destination account ID)\",", + " \"destinationAccount\": \"object (Destination account object)\",", + " \"sourceAmount\": \"integer (Source amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)\",", + " \"destinationAmount\": \"integer (Destination amount, supports up to two decimals. For example, a value of 1234 represents an amount of 12.34)\",", + " \"hideAmount\": \"boolean (Whether to hide the amount)\",", + " \"tagIds\": [\"each string representing a transaction tag ID\"],", + " \"tags\": [\"each object representing a transaction tag object\"],", + " \"pictures\": [\"each object representing a transaction picture object\"],", + " \"comment\": \"string (Transaction description)\",", + " \"geoLocation\": \"object (Transaction geographic location)\",", + " \"editable\": \"boolean (Whether the transaction is editable)\"", + " }", + "]" + ] + }, + { + "Name": "transactions-add", + "Description": "Add transaction", + "Method": "POST", + "Path": "transactions/add.json", + "RequiresTimezone": true, + "RequiredParams": ["type", "categoryId", "time", "utcOffset", "sourceAccountId", "sourceAmount"], + "OptionalParams": ["destinationAccountId", "destinationAmount", "hideAmount", "tagIds", "pictureIds", "comment", "geoLocation"], + "ParamTypes": { + "type": "integer", + "categoryId": "string", + "time": "integer", + "utcOffset": "integer", + "sourceAccountId": "string", + "sourceAmount": "integer", + "destinationAccountId": "string", + "destinationAmount": "integer", + "hideAmount": "boolean", + "tagIds": "string_array", + "pictureIds": "string_array", + "comment": "string", + "geoLocation": "geo_location" + }, + "ParamDescriptions": { + "type": "integer (Transaction type, 1: Balance Modification, 2: Income, 3: Expense, 4: Transfer)", + "categoryId": "string (Transaction category ID)", + "time": "integer (Transaction unix time)", + "utcOffset": "integer (Transaction time zone offset minutes)", + "sourceAccountId": "string (Source account ID)", + "sourceAmount": "integer (Source amount, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\")", + "destinationAccountId": "string (Destination account ID)", + "destinationAmount": "integer (Destination amount, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\")", + "hideAmount": "boolean (Whether to hide amount)", + "tagIds": "string (Transaction tag IDs, separated by comma, e.g. \"tagid1,tagid2\")", + "pictureIds": "string (Transaction picture IDs, separated by comma, e.g. \"picid1,picid2\")", + "comment": "string (Transaction description)", + "geoLocation": "string (Transaction geographic location, format: longitude,latitude, e.g. \"116.33,39.93\")" + }, + "ResponseStructure": [ + "{", + " \"id\": \"string (Transaction ID)\",", + " \"timeSequenceId\": \"string (Transaction time sequence ID)\",", + " \"type\": \"integer (Transaction type)\",", + " \"categoryId\": \"string (Transaction category ID)\",", + " \"category\": \"object (Transaction category object)\",", + " \"time\": \"integer (Transaction unix time)\",", + " \"utcOffset\": \"integer (Transaction time zone offset minutes)\",", + " \"sourceAccountId\": \"string (Source account ID)\",", + " \"sourceAccount\": \"object (Source account object)\",", + " \"destinationAccountId\": \"string (Destination account ID)\",", + " \"destinationAccount\": \"object (Destination account object)\",", + " \"sourceAmount\": \"integer (Source amount)\",", + " \"destinationAmount\": \"integer (Destination amount)\",", + " \"hideAmount\": \"boolean (Whether to hide the amount)\",", + " \"tagIds\": [\"each string representing a transaction tag ID\"],", + " \"tags\": [\"each object representing a transaction tag object\"],", + " \"pictures\": [\"each object representing a transaction picture object\"],", + " \"comment\": \"string (Transaction description)\",", + " \"geoLocation\": \"object (Transaction geographic location)\",", + " \"editable\": \"boolean (Whether the transaction is editable)\"", + "}" + ] + } +]' + +TIMEZONE_NAME="" +TIMEZONE_OFFSET="" + +echo_red() { + printf '\033[31m%s\033[0m\n' "$1" +} + +echo_yellow() { + printf '\033[33m%s\033[0m\n' "$1" +} + +check_dependency() { + for cmd in $1 + do + if ! command -v "$cmd" > /dev/null 2>&1; then + echo_red "Error: \"$cmd\" is required." + exit 127 + fi + done +} + +url_encode() { + text="$1" + printf "%s" "$text" | jq -sRr @uri +} + +get_system_timezone_name() { + if [ -f /etc/timezone ]; then + cat /etc/timezone + elif [ -L /etc/localtime ]; then + readlink /etc/localtime | sed 's|.*/zoneinfo/||' + elif command -v timedatectl > /dev/null 2>&1; then + timedatectl | grep "Time zone" | awk '{print $3}' + else + echo "Asia/Shanghai" + fi +} + +get_system_timezone_offset() { + offset_str="$(date +%z 2>/dev/null || echo "+0800")" + sign="${offset_str%????}" + hours="${offset_str#?}" + hours="${hours%??}" + minutes="${offset_str#???}" + + hours=$(echo "$hours" | sed 's/^0*//') + minutes=$(echo "$minutes" | sed 's/^0*//') + + [ -z "$hours" ] && hours=0 + [ -z "$minutes" ] && minutes=0 + + if [ "$sign" = "+" ]; then + echo "$((hours * 60 + minutes))" + else + echo "$((-(hours * 60 + minutes)))" + fi +} + +parse_api_config() { + api_name="$1" + echo "$API_CONFIGS" | jq -r --arg name "$api_name" '.[] | select(.Name == $name)' +} + +show_help() { + tz_name="$(get_system_timezone_name)" + tz_offset="$(get_system_timezone_offset)" + + cat <<-EOF +ezBookkeeping API Tools + +A command-line tool for calling ezBookkeeping APIs + +Usage: + ebktools.sh [--tz-name ] [--tz-offset ] [command-options] + +Global Options: + --tz-name The IANA timezone name, for example, for Beijing Time it is 'Asia/Shanghai'. + --tz-offset The offset in minutes of the current timezone from UTC. For example, for Beijing Time which is UTC+8, the value is '480'. If both are provided, '--tz-name' takes precedence. + +Environment Variables (Required): + EBKTOOL_SERVER_BASEURL ezBookkeeping server base URL (e.g., http://localhost:8080) + EBKTOOL_TOKEN ezBookkeeping API token + +Commands: + list List all available API commands + help Show help for a specific API command + Execute an API command + +Examples: + # Set environment variables + export EBKTOOL_SERVER_BASEURL="http://localhost:8080" + export EBKTOOL_TOKEN="YOUR_TOKEN" + + # List all available commands + ebktools.sh list + + # Show help for a specific command + ebktools.sh help transactions-add + + # Call accounts-list API + ebktools.sh accounts-list + + # Call API with timezone name + ebktools.sh --tz-name ${tz_name} transactions-list --count 10 + + # Call API with timezone offset + ebktools.sh --tz-offset ${tz_offset} transactions-list --count 10 +EOF +} + +list_commands() { + echo "Available API Commands:" + echo "" + + echo "$API_CONFIGS" | jq -r '.[] | "\(.Name)|\(.Description)"' | while IFS='|' read -r name desc; do + printf " %-30s %s\n" "$name" "$desc" + done + + echo "" + echo "Use 'ebktools.sh help ' to see detailed information about a API command." +} + +show_command_help() { + command_name="$1" + config="$(parse_api_config "$command_name")" + + if [ -z "$config" ]; then + echo_red "Error: Unknown command '$command_name'" + echo "" + echo "Use 'ebktools.sh list' to see all available commands." + exit 1 + fi + + name="$(echo "$config" | jq -r '.Name')" + desc="$(echo "$config" | jq -r '.Description')" + method="$(echo "$config" | jq -r '.Method')" + path="$(echo "$config" | jq -r '.Path')" + response_struct="$(echo "$config" | jq -r '.ResponseStructure')" + + echo "Command: $name" + echo "Description: $desc" + echo "Method: $method" + echo "Path: $path" + echo "" + + required_count="$(echo "$config" | jq '.RequiredParams | length')" + if [ "$required_count" -gt 0 ]; then + echo "Required Parameters:" + echo "$config" | jq -r '.RequiredParams[]' | while read -r param; do + param_desc="$(echo "$config" | jq -r --arg p "$param" '.ParamDescriptions[$p] // ""')" + printf " --%-25s %s\n" "$param" "$param_desc" + done + echo "" + fi + + optional_count="$(echo "$config" | jq '.OptionalParams | length')" + if [ "$optional_count" -gt 0 ]; then + echo "Optional Parameters:" + echo "$config" | jq -r '.OptionalParams[]' | while read -r param; do + param_desc="$(echo "$config" | jq -r --arg p "$param" '.ParamDescriptions[$p] // ""')" + printf " --%-25s %s\n" "$param" "$param_desc" + done + echo "" + fi + + response_struct="$(echo "$config" | jq -r '.ResponseStructure')" + + if [ -n "$response_struct" ] && [ "$response_struct" != "null" ]; then + echo "Response Structure:" + echo "$config" | jq -r '.ResponseStructure[]' | while IFS= read -r line; do + echo " $line" + done + echo "" + fi + + echo "Example:" + requires_timezone="$(echo "$config" | jq -r '.RequiresTimezone // false')" + if [ "$requires_timezone" = "true" ]; then + tz_name="$(get_system_timezone_name)" + echo " ebktools.sh --tz-name ${tz_name} $name" + else + echo " ebktools.sh $name" + fi +} + +call_api() { + command_name="$1" + shift + + config="$(parse_api_config "$command_name")" + + if [ -z "$config" ]; then + echo_red "Error: Unknown command '$command_name'" + echo "" + echo "Use 'ebktools.sh list' to see all available commands." + exit 1 + fi + + serverBaseUrl="$EBKTOOL_SERVER_BASEURL" + authToken="$EBKTOOL_TOKEN" + + if [ -z "$serverBaseUrl" ]; then + echo_red "Error: Environment variable 'EBKTOOL_SERVER_BASEURL' is not set." + echo "Please set it to your API server base URL (e.g., http://localhost:8080)" + exit 1 + fi + + if [ -z "$authToken" ]; then + echo_red "Error: Environment variable 'EBKTOOL_TOKEN' is not set." + echo "Please set it to your authentication token." + exit 1 + fi + + requires_timezone="$(echo "$config" | jq -r '.RequiresTimezone // false')" + if [ "$requires_timezone" = "true" ] && [ -z "$TIMEZONE_NAME" ] && [ -z "$TIMEZONE_OFFSET" ]; then + tz_name="$(get_system_timezone_name)" + tz_offset="$(get_system_timezone_offset)" + echo_red "Error: Command '$command_name' requires timezone information." + echo "Please provide either '--tz-name' or '--tz-offset' parameter." + echo "" + echo "Examples:" + echo " ebktools.sh --tz-name ${tz_name} $command_name ..." + echo " ebktools.sh --tz-offset ${tz_offset} $command_name ..." + exit 1 + fi + + method="$(echo "$config" | jq -r '.Method')" + path="$(echo "$config" | jq -r '.Path')" + + get_param_type() { + param_name="$1" + param_type="$(echo "$config" | jq -r --arg p "$param_name" '.ParamTypes[$p] // "string"')" + echo "$param_type" + } + + validate_param() { + param_name="$1" + param_value="$2" + param_type="$3" + + case "$param_type" in + integer) + if ! echo "$param_value" | grep -Eq '^-?[0-9]+$'; then + echo_red "Error: Parameter '--${param_name}' must be a integer value" + exit 1 + fi + ;; + boolean) + if ! echo "$param_value" | grep -Eq '^(true|false|1|0)$'; then + echo_red "Error: Parameter '--${param_name}' must be a boolean value (true/false or 1/0)" + exit 1 + fi + ;; + geo_location) + if ! echo "$param_value" | grep -Eq '^-?[0-9]+(\.[0-9]+)?,-?[0-9]+(\.[0-9]+)?$'; then + echo_red "Error: Parameter '--${param_name}' must be in format 'longitude,latitude'" + exit 1 + fi + ;; + esac + } + + params="" + json_params="{}" + while [ $# -gt 0 ]; do + case "${1}" in + --*) + param_name="$(echo "${1}" | sed 's/^--//')" + + if [ $# -lt 2 ]; then + echo_red "Error: Parameter '--${param_name}' requires a value" + exit 1 + fi + + case "$2" in + --*) + echo_red "Error: Parameter '--${param_name}' requires a value" + exit 1 + ;; + esac + + param_value="$2" + param_type="$(get_param_type "$param_name")" + + validate_param "$param_name" "$param_value" "$param_type" + + encoded_value="$(url_encode "$param_value")" + if [ -z "$params" ]; then + params="${param_name}=${encoded_value}" + else + params="${params}&${param_name}=${encoded_value}" + fi + + case "$param_type" in + integer) + json_params="$(echo "$json_params" | jq --arg k "$param_name" --argjson v "$param_value" '. + {($k): $v}')" + ;; + boolean) + if [ "$param_value" = "true" ] || [ "$param_value" = "1" ]; then + bool_value="true" + else + bool_value="false" + fi + json_params="$(echo "$json_params" | jq --arg k "$param_name" --argjson v "$bool_value" '. + {($k): $v}')" + ;; + string_array) + json_array="[]" + old_ifs="$IFS" + IFS=',' + for val in $param_value; do + json_array="$(echo "$json_array" | jq --arg v "$val" '. + [$v]')" + done + IFS="$old_ifs" + json_params="$(echo "$json_params" | jq --arg k "$param_name" --argjson v "$json_array" '. + {($k): $v}')" + ;; + geo_location) + longitude="$(echo "$param_value" | cut -d',' -f1)" + latitude="$(echo "$param_value" | cut -d',' -f2)" + geo_json="$(jq -n --arg lat "$latitude" --arg lon "$longitude" '{latitude: ($lat | tonumber), longitude: ($lon | tonumber)}')" + json_params="$(echo "$json_params" | jq --arg k "$param_name" --argjson v "$geo_json" '. + {($k): $v}')" + ;; + *) + json_params="$(echo "$json_params" | jq --arg k "$param_name" --arg v "$param_value" '. + {($k): $v}')" + ;; + esac + shift 2 + ;; + *) + echo_red "Error: Invalid parameter: '$1'" + exit 1 + ;; + esac + done + + required_count="$(echo "$config" | jq '.RequiredParams | length')" + if [ "$required_count" -gt 0 ]; then + i=0 + while [ "$i" -lt "$required_count" ]; do + param="$(echo "$config" | jq -r --argjson idx "$i" '.RequiredParams[$idx]')" + if ! echo "$params" | grep -q "${param}="; then + echo_red "Error: Required parameter '--${param}' is missing" + exit 1 + fi + i=$((i + 1)) + done + fi + + if [ "${serverBaseUrl%/}" != "$serverBaseUrl" ]; then + serverBaseUrl="${serverBaseUrl%/}" + fi + + url="${serverBaseUrl}/api/v1/${path}" + timezone_headers="" + + if [ -n "$TIMEZONE_NAME" ]; then + timezone_headers="X-Timezone-Name: $TIMEZONE_NAME" + elif [ -n "$TIMEZONE_OFFSET" ]; then + timezone_headers="X-Timezone-Offset: $TIMEZONE_OFFSET" + fi + + if [ "$method" = "POST" ]; then + echo_yellow "Calling API: $method $url" + echo "" + + if [ "$json_params" != "{}" ]; then + if [ -n "$timezone_headers" ]; then + response=$(curl -s -X "POST" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + -H "Content-Type: application/json" \ + -H "$timezone_headers" \ + -d "$json_params" \ + "$url") + curl_exit_code=$? + else + response=$(curl -s -X "POST" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$json_params" \ + "$url") + curl_exit_code=$? + fi + else + if [ -n "$timezone_headers" ]; then + response=$(curl -s -X "POST" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + -H "$timezone_headers" \ + "$url") + curl_exit_code=$? + else + response=$(curl -s -X "POST" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + "$url") + curl_exit_code=$? + fi + fi + else + if [ -n "$params" ]; then + url="${url}?${params}" + fi + + echo_yellow "Calling API: $method $url" + echo "" + + if [ -n "$timezone_headers" ]; then + response=$(curl -s -X "$method" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + -H "$timezone_headers" \ + "$url") + else + response=$(curl -s -X "$method" \ + -H "Authorization: Bearer $EBKTOOL_TOKEN" \ + "$url") + fi + curl_exit_code=$? + fi + + if [ "$curl_exit_code" -ne 0 ]; then + echo_red "Error: API call failed (curl exit code: $curl_exit_code)" + exit 1 + fi + + success=$(echo "$response" | jq -r '.success // "null"') + + if [ "$success" = "true" ]; then + echo "Response Result:" + result=$(echo "$response" | jq '.result // "null"') + if [ "$result" != "null" ]; then + echo "$response" | jq '.result' + else + echo "Success: true (No result data)" + fi + elif [ "$success" = "false" ]; then + echo "Raw Response:" + echo "$response" | jq '.' + else + echo "Raw Response:" + echo "$response" | jq '.' + fi +} + +main() { + check_dependency "grep sed awk date curl jq" + + COMMAND="" + + while [ $# -gt 0 ]; do + case "${1}" in + --tz-name) + if [ $# -lt 2 ]; then + echo_red "Error: '--tz-name' requires a value" + exit 1 + fi + TIMEZONE_NAME="$2" + shift 2 + ;; + --tz-offset) + if [ $# -lt 2 ]; then + echo_red "Error: '--tz-offset' requires a value" + exit 1 + fi + TIMEZONE_OFFSET="$2" + shift 2 + ;; + --help | -h) + show_help + exit 0 + ;; + list) + list_commands + exit 0 + ;; + help) + if [ -z "$2" ]; then + show_help + else + show_command_help "$2" + fi + exit 0 + ;; + --*) + echo_red "Error: Unknown option: $1" + show_help + exit 1 + ;; + *) + COMMAND="$1" + shift + break + ;; + esac + done + + if [ -z "$COMMAND" ]; then + show_help + exit 0 + fi + + call_api "$COMMAND" "$@" +} + +main "$@"