diff options
| author | frosty <gabriel@bwaaa.monster> | 2026-03-13 13:33:30 -0400 |
|---|---|---|
| committer | frosty <gabriel@bwaaa.monster> | 2026-03-13 13:33:30 -0400 |
| commit | 4539593e6ad442dc2547ccab38a3c642ccd635e1 (patch) | |
| tree | f291a5f603e0468650167f8aaba82d8c8135028e /src/Infobox/CurrencyConversion.c | |
| parent | ef1cd7cc6060df0f5dcfada136dd99a695e8a0fc (diff) | |
| download | omnisearch-4539593e6ad442dc2547ccab38a3c642ccd635e1.tar.gz | |
feature: added currency conversion infobox
Diffstat (limited to 'src/Infobox/CurrencyConversion.c')
| -rw-r--r-- | src/Infobox/CurrencyConversion.c | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/src/Infobox/CurrencyConversion.c b/src/Infobox/CurrencyConversion.c new file mode 100644 index 0000000..f9d1fa6 --- /dev/null +++ b/src/Infobox/CurrencyConversion.c @@ -0,0 +1,474 @@ +#include "CurrencyConversion.h" +#include "../Cache/Cache.h" +#include "../Utility/HttpClient.h" +#include "../Utility/JsonHelper.h" +#include <ctype.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct { + const char *code; + const char *name; + const char *symbol; + int is_crypto; +} CurrencyDef; + +static const CurrencyDef CURRENCIES[] = { + {"USD", "US Dollar", "$", 0}, + {"EUR", "Euro", "€", 0}, + {"GBP", "British Pound", "£", 0}, + {"JPY", "Japanese Yen", "¥", 0}, + {"AUD", "Australian Dollar", "A$", 0}, + {"CAD", "Canadian Dollar", "C$", 0}, + {"CHF", "Swiss Franc", "CHF", 0}, + {"CNY", "Chinese Yuan", "¥", 0}, + {"INR", "Indian Rupee", "₹", 0}, + {"KRW", "South Korean Won", "₩", 0}, + {"BRL", "Brazilian Real", "R$", 0}, + {"RUB", "Russian Ruble", "₽", 0}, + {"ZAR", "South African Rand", "R", 0}, + {"MXN", "Mexican Peso", "MX$", 0}, + {"SGD", "Singapore Dollar", "S$", 0}, + {"HKD", "Hong Kong Dollar", "HK$", 0}, + {"NOK", "Norwegian Krone", "kr", 0}, + {"SEK", "Swedish Krona", "kr", 0}, + {"DKK", "Danish Krone", "kr", 0}, + {"NZD", "New Zealand Dollar", "NZ$", 0}, + {"TRY", "Turkish Lira", "₺", 0}, + {"PLN", "Polish Zloty", "zł", 0}, + {"THB", "Thai Baht", "฿", 0}, + {"IDR", "Indonesian Rupiah", "Rp", 0}, + {"HUF", "Hungarian Forint", "Ft", 0}, + {"CZK", "Czech Koruna", "Kč", 0}, + {"ILS", "Israeli Shekel", "₪", 0}, + {"CLP", "Chilean Peso", "CLP$", 0}, + {"PHP", "Philippine Peso", "₱", 0}, + {"AED", "UAE Dirham", "د.إ", 0}, + {"COP", "Colombian Peso", "COP$", 0}, + {"SAR", "Saudi Riyal", "﷼", 0}, + {"MYR", "Malaysian Ringgit", "RM", 0}, + {"RON", "Romanian Leu", "lei", 0}, + {"ARS", "Argentine Peso", "ARS$", 0}, + {"PKR", "Pakistani Rupee", "₨", 0}, + {"EGP", "Egyptian Pound", "£", 0}, + {"VND", "Vietnamese Dong", "₫", 0}, + {"NGN", "Nigerian Naira", "₦", 0}, + {"BDT", "Bangladeshi Taka", "৳", 0}, + {"UAH", "Ukrainian Hryvnia", "₴", 0}, + {"PEN", "Peruvian Sol", "S/", 0}, + {"BGN", "Bulgarian Lev", "лв", 0}, + {"HRK", "Croatian Kuna", "kn", 0}, + {"ISK", "Icelandic Krona", "kr", 0}, + {"MAD", "Moroccan Dirham", "د.م.", 0}, + {"KES", "Kenyan Shilling", "KSh", 0}, + {"QAR", "Qatari Riyal", "﷼", 0}, + {"KWD", "Kuwaiti Dinar", "د.ك", 0}, + {"BHD", "Bahraini Dinar", ".د.ب", 0}, + {"OMR", "Omani Rial", "﷼", 0}, + {"JOD", "Jordanian Dinar", "د.ا", 0}, + {"TWD", "Taiwan Dollar", "NT$", 0}, + + {"BTC", "Bitcoin", "₿", 1}, + {"ETH", "Ethereum", "Ξ", 1}, + {"USDT", "Tether", "₮", 1}, + {"BNB", "Binance Coin", "BNB", 1}, + {"XRP", "Ripple", "XRP", 1}, + {"USDC", "USD Coin", "$", 1}, + {"ADA", "Cardano", "ADA", 1}, + {"DOGE", "Dogecoin", "Ð", 1}, + {"SOL", "Solana", "SOL", 1}, + {"TRX", "Tron", "TRX", 1}, + {"DOT", "Polkadot", "DOT", 1}, + {"MATIC", "Polygon", "MATIC", 1}, + {"LTC", "Litecoin", "Ł", 1}, + {"SHIB", "Shiba Inu", "SHIB", 1}, + {"AVAX", "Avalanche", "AVAX", 1}, + {"LINK", "Chainlink", "LINK", 1}, + {"ATOM", "Cosmos", "ATOM", 1}, + {"XMR", "Monero", "XMR", 1}, + {"ETC", "Ethereum Classic", "ETC", 1}, + {"XLM", "Stellar", "XLM", 1}, + {"BCH", "Bitcoin Cash", "BCH", 1}, + {"ALGO", "Algorand", "ALGO", 1}, + {"VET", "VeChain", "VET", 1}, + {"FIL", "Filecoin", "FIL", 1}, + {"NEAR", "NEAR Protocol", "NEAR", 1}, + {"APT", "Aptos", "APT", 1}, + {"ARB", "Arbitrum", "ARB", 1}, + {"OP", "Optimism", "OP", 1}, + {"SAND", "The Sandbox", "SAND", 1}, + {"MANA", "Decentraland", "MANA", 1}, + {"AXS", "Axie Infinity", "AXS", 1}, + {"AAVE", "Aave", "AAVE", 1}, + {"MKR", "Maker", "MKR", 1}, + {"GRT", "The Graph", "GRT", 1}, + {"FTM", "Fantom", "FTM", 1}, + {"CRO", "Cronos", "CRO", 1}, + {"NEAR", "NEAR Protocol", "NEAR", 1}, + {"INJ", "Injective", "INJ", 1}, + {"RUNE", "THORChain", "RUNE", 1}, + {"LDO", "Lido DAO", "LDO", 1}, + {"QNT", "Quant", "QNT", 1}, + {"RNDR", "Render", "RNDR", 1}, + {"STX", "Stacks", "STX", 1}, + {"IMX", "Immutable X", "IMX", 1}, + {"SNX", "Synthetix", "SNX", 1}, + {"THETA", "Theta", "THETA", 1}, + {"XTZ", "Tezos", "XTZ", 1}, + {"EOS", "EOS", "EOS", 1}, + {"FLOW", "Flow", "FLOW", 1}, + {"CHZ", "Chiliz", "CHZ", 1}, + {"CRV", "Curve DAO", "CRV", 1}, + {"APE", "ApeCoin", "APE", 1}, + {"PEPE", "Pepe", "PEPE", 1}, + {"WIF", "dogwifhat", "WIF", 1}, + {"SUI", "Sui", "SUI", 1}, + {"SEI", "Sei", "SEI", 1}, + {"TIA", "Celestia", "TIA", 1}, + {"PYTH", "Pyth Network", "PYTH", 1}, + {"BONK", "Bonk", "BONK", 1}, + {"FET", "Fetch.ai", "FET", 1}, + {"AGIX", "SingularityNET", "AGIX", 1}, + {"RNDR", "Render Token", "RNDR", 1}, +}; + +static const int CURRENCY_COUNT = sizeof(CURRENCIES) / sizeof(CURRENCIES[0]); + +static int is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static const CurrencyDef *find_currency(const char *str) { + if (!str || !*str) + return NULL; + + size_t len = strlen(str); + if (len < 2 || len > 10) + return NULL; + + char normalized[16] = {0}; + size_t j = 0; + for (size_t i = 0; i < len && j < 15; i++) { + normalized[j++] = toupper((unsigned char)str[i]); + } + normalized[j] = '\0'; + + for (int i = 0; i < CURRENCY_COUNT; i++) { + if (strcmp(normalized, CURRENCIES[i].code) == 0) { + return &CURRENCIES[i]; + } + } + return NULL; +} + +int is_currency_query(const char *query) { + if (!query) + return 0; + + const char *patterns[] = {" to ", " in ", " into ", " = ", " equals ", NULL}; + + int has_pattern = 0; + for (int i = 0; patterns[i]; i++) { + if (strstr(query, patterns[i])) { + has_pattern = 1; + break; + } + } + + if (!has_pattern) { + const char *last_space = strrchr(query, ' '); + if (last_space) { + const CurrencyDef *c = find_currency(last_space + 1); + if (c) { + const char *before = query; + while (*before && is_whitespace(*before)) + before++; + const char *num_end = before; + while (*num_end && + (isdigit(*num_end) || *num_end == '.' || *num_end == ',' || + *num_end == '-' || *num_end == '+')) { + num_end++; + } + if (num_end > before) + has_pattern = 1; + } + } + } + + return has_pattern; +} + +static double parse_value(const char **ptr) { + const char *p = *ptr; + while (*p && is_whitespace(*p)) + p++; + + double value = 0.0; + int has_decimal = 0; + double decimal_place = 1.0; + + if (*p == '-' || *p == '+') + p++; + + while (*p >= '0' && *p <= '9') { + value = value * 10 + (*p - '0'); + if (has_decimal) + decimal_place *= 10; + p++; + } + + if (*p == '.' || *p == ',') { + has_decimal = 1; + p++; + while (*p >= '0' && *p <= '9') { + value = value * 10 + (*p - '0'); + decimal_place *= 10; + p++; + } + } + + if (has_decimal) + value /= decimal_place; + + *ptr = p; + return value; +} + +static int parse_currency_query(const char *query, double *value, + const CurrencyDef **from_curr, + const CurrencyDef **to_curr) { + *value = 0; + *from_curr = NULL; + *to_curr = NULL; + + const char *value_end = query; + *value = parse_value(&value_end); + + if (value_end == query) + return 0; + + const char *p = value_end; + while (*p && is_whitespace(*p)) + p++; + + size_t remaining = strlen(p); + if (remaining < 2) + return 0; + + const char *to_keywords[] = {" to ", " in ", " into ", " -> ", + " → ", " = ", NULL}; + const char *to_pos = NULL; + size_t keyword_len = 0; + for (int i = 0; to_keywords[i]; i++) { + const char *found = strstr(p, to_keywords[i]); + if (found) { + to_pos = found + strlen(to_keywords[i]); + keyword_len = strlen(to_keywords[i]); + break; + } + } + + if (!to_pos) { + const char *last_space = strrchr(p, ' '); + if (last_space && last_space > p) { + char from_part[32] = {0}; + size_t len = last_space - p; + if (len < 31) { + strncpy(from_part, p, len); + *from_curr = find_currency(from_part); + if (*from_curr) { + *to_curr = find_currency(last_space + 1); + return *to_curr ? 1 : 0; + } + } + } + return 0; + } + + char from_part[32] = {0}; + size_t from_len = to_pos - p - keyword_len; + if (from_len > 31) + from_len = 31; + strncpy(from_part, p, from_len); + + char *end_from = from_part + from_len; + while (end_from > from_part && is_whitespace(end_from[-1])) + end_from--; + *end_from = '\0'; + + *from_curr = find_currency(from_part); + if (!*from_curr) { + return 0; + } + + while (*to_pos && is_whitespace(*to_pos)) + to_pos++; + + if (!*to_pos) + return 0; + + char to_part[32] = {0}; + size_t to_len = 0; + const char *tp = to_pos; + while (*tp && !is_whitespace(*tp) && to_len < 31) { + to_part[to_len++] = *tp++; + } + to_part[to_len] = '\0'; + + *to_curr = find_currency(to_part); + if (!*to_curr) { + char try_buf[32] = {0}; + strncpy(try_buf, to_pos, 31); + *to_curr = find_currency(try_buf); + } + + return *to_curr ? 1 : 0; +} + +static double get_rate_from_json(const char *json, const char *target_code) { + JsonFloatMap map; + if (json_parse_float_map(json, "rates", &map)) { + for (size_t i = 0; i < map.count; i++) { + if (strcmp(map.keys[i], target_code) == 0) { + return map.values[i]; + } + } + } + return 0; +} + +static void format_number(double val, char *buf, size_t bufsize) { + if (bufsize == 0) + return; + if (val == 0) { + snprintf(buf, bufsize, "0"); + return; + } + if (fabs(val) < 0.01 && fabs(val) > 0) { + snprintf(buf, bufsize, "%.6f", val); + } else if (fabs(val) < 1) { + snprintf(buf, bufsize, "%.4f", val); + char *p = buf + strlen(buf) - 1; + while (p > buf && (*p == '0' || *p == '.')) { + if (*p == '.') + break; + *p-- = '\0'; + } + } else if (fmod(val + 0.0001, 1.0) < 0.0002) { + snprintf(buf, bufsize, "%.0f", val); + } else { + snprintf(buf, bufsize, "%.2f", val); + char *p = buf + strlen(buf) - 1; + while (p > buf && (*p == '0' || *p == '.')) { + if (*p == '.') + break; + *p-- = '\0'; + } + } +} + +static char *build_html(double value, const CurrencyDef *from, + const CurrencyDef *to, double result) { + static char html[4096]; + char val_buf[64], res_buf[64]; + + format_number(value, val_buf, sizeof(val_buf)); + format_number(result, res_buf, sizeof(res_buf)); + + snprintf(html, sizeof(html), + "<div class='currency-conv-container' style='line-height: 1.6;'>" + "<div style='font-size: 1.3em; margin-bottom: 8px;'>" + "<b>%s %s</b> = <b>%s %s</b></div>" + "<div style='font-size: 0.9em; color: #666;'>" + "1 %s = %.4f %s</div>" + "</div>", + val_buf, from->code, res_buf, to->code, from->code, result / value, + to->code); + + return html; +} + +InfoBox fetch_currency_data(const char *query) { + InfoBox info = {NULL, NULL, NULL, NULL}; + if (!query) + return info; + + double value = 0; + const CurrencyDef *from_curr = NULL; + const CurrencyDef *to_curr = NULL; + + if (!parse_currency_query(query, &value, &from_curr, &to_curr)) + return info; + if (!from_curr || !to_curr) + return info; + if (strcmp(from_curr->code, to_curr->code) == 0) + return info; + + char cache_key[128]; + snprintf(cache_key, sizeof(cache_key), "currency_%s_%s", from_curr->code, + to_curr->code); + + char *cached_data = NULL; + size_t cached_size = 0; + double rate = 0; + int use_cache = 0; + + int is_crypto = from_curr->is_crypto || to_curr->is_crypto; + + if (get_cache_ttl_infobox() > 0) { + if (cache_get(cache_key, get_cache_ttl_infobox(), &cached_data, + &cached_size) == 0 && + cached_data && cached_size > 0) { + if (is_crypto) { + rate = json_get_float(cached_data, to_curr->code); + } else { + rate = get_rate_from_json(cached_data, to_curr->code); + } + if (rate > 0) { + use_cache = 1; + } + free(cached_data); + } + } + + if (!use_cache) { + char url[512]; + + if (is_crypto) { + snprintf(url, sizeof(url), + "https://min-api.cryptocompare.com/data/price?fsym=%s&tsyms=%s", + from_curr->code, to_curr->code); + } else { + snprintf(url, sizeof(url), + "https://api.exchangerate-api.com/v4/latest/%s", + from_curr->code); + } + + HttpResponse resp = http_get(url, "libcurl-agent/1.0"); + if (resp.memory && resp.size > 0) { + if (is_crypto) { + rate = json_get_float(resp.memory, to_curr->code); + } else { + rate = get_rate_from_json(resp.memory, to_curr->code); + } + if (rate > 0 && get_cache_ttl_infobox() > 0) { + cache_set(cache_key, resp.memory, resp.size); + } + } + http_response_free(&resp); + } + + if (rate <= 0) + return info; + + double result = value * rate; + + info.title = strdup("Currency Conversion"); + info.extract = strdup(build_html(value, from_curr, to_curr, result)); + info.thumbnail_url = strdup("/static/calculation.svg"); + info.url = strdup("#"); + + return info; +} |
