diff --git a/api/cache/crypto_prices.json b/api/cache/crypto_prices.json new file mode 100644 index 0000000..dc837a9 --- /dev/null +++ b/api/cache/crypto_prices.json @@ -0,0 +1 @@ +{"LTC": 64.665, "_last_updated": 1695334741.4905503, "BTC": 26583.485, "MATIC": 0.52075, "XMR": 146.46058828041404, "ADA": 0.2455, "USDT": 1.000005, "ETH": 1586.115, "USD": 1.0, "EUR": 1.0662838016640013} \ No newline at end of file diff --git a/api/models.json b/api/cache/models.json similarity index 100% rename from api/models.json rename to api/cache/models.json diff --git a/api/core.py b/api/core.py index a53320d..c88b732 100644 --- a/api/core.py +++ b/api/core.py @@ -8,9 +8,12 @@ sys.path.append(project_root) # the code above is to allow importing from the root folder +import time import json import hmac +import httpx import fastapi +import functools from dhooks import Webhook, Embed from dotenv import load_dotenv @@ -18,7 +21,7 @@ from dotenv import load_dotenv import checks.client from helpers import errors -from db.users import UserManager +from db import users, finances load_dotenv() router = fastapi.APIRouter(tags=['core']) @@ -62,9 +65,7 @@ async def get_users(discord_id: int, incoming_request: fastapi.Request): auth = await check_core_auth(incoming_request) if auth: return auth - # Get user by discord ID - manager = UserManager() - user = await manager.user_by_discord_id(discord_id) + user = await users.manager.user_by_discord_id(discord_id) if not user: return await errors.error(404, 'Discord user not found in the API database.', 'Check the `discord_id` parameter.') @@ -88,9 +89,7 @@ async def create_user(incoming_request: fastapi.Request): except (json.decoder.JSONDecodeError, AttributeError): return await errors.error(400, 'Invalid or no payload received.', 'The payload must be a JSON object with a `discord_id` key.') - # Create the user - manager = UserManager() - user = await manager.create(discord_id) + user = await users.manager.create(discord_id) await new_user_webhook(user) user['_id'] = str(user['_id']) @@ -114,9 +113,7 @@ async def update_user(incoming_request: fastapi.Request): 'The payload must be a JSON object with a `discord_id` key and an `updates` key.' ) - # Update the user - manager = UserManager() - user = await manager.update_by_discord_id(discord_id, updates) + user = await users.manager.update_by_discord_id(discord_id, updates) return user @@ -147,3 +144,54 @@ async def run_checks(incoming_request: fastapi.Request): results[func.__name__] = result return results + +async def get_crypto_price(cryptocurrency: str) -> float: + """Gets the price of a cryptocurrency using coinbase's API.""" + + if os.path.exists('cache/crypto_prices.json'): + with open('cache/crypto_prices.json', 'r') as f: + cache = json.load(f) + else: + cache = {} + + is_old = time.time() - cache.get('_last_updated', 0) > 60 * 60 + + if is_old or cryptocurrency not in cache: + async with httpx.AsyncClient() as client: + response = await client.get(f'https://api.coinbase.com/v2/prices/{cryptocurrency}-USD/spot') + usd_price = float(response.json()['data']['amount']) + + cache[cryptocurrency] = usd_price + cache['_last_updated'] = time.time() + + with open('cache/crypto_prices.json', 'w') as f: + json.dump(cache, f) + + return cache[cryptocurrency] + +@router.get('/finances') +async def get_finances(incoming_request: fastapi.Request): + """Return financial information. Requires a core API key.""" + + auth_error = await check_core_auth(incoming_request) + if auth_error: return auth_error + + transactions = await finances.manager.get_entire_financial_history() + + for table in transactions: + for transaction in transactions[table]: + currency = transaction['currency'] + + if '-' in currency: + currency = currency.split('-')[0] + + amount = transaction['amount'] + + if currency == 'mBTC': + currency = 'BTC' + amount = transaction['amount'] / 1000 + + amount_in_usd = await get_crypto_price(currency) * amount + transactions[table][transactions[table].index(transaction)]['amount_usd'] = amount_in_usd + + return transactions diff --git a/api/db/finances.py b/api/db/finances.py new file mode 100644 index 0000000..b4bf252 --- /dev/null +++ b/api/db/finances.py @@ -0,0 +1,39 @@ +import os +import asyncio + +from dotenv import load_dotenv +from motor.motor_asyncio import AsyncIOMotorClient + +load_dotenv() + +class FinanceManager: + def __init__(self): + self.conn = AsyncIOMotorClient(os.environ['MONGO_URI']) + + async def _get_collection(self, collection_name: str): + return self.conn['finances'][collection_name] + + async def get_entire_financial_history(self): + donations_db = await self._get_collection('donations') + expenses_db = await self._get_collection('expenses') + + # turn both into JSON-like lists of dicts at once (make sure to fix the _id) + history = {'donations': [], 'expenses': []} + + async for donation in donations_db.find(): + donation['_id'] = str(donation['_id']) + history['donations'].append(donation) + + async for expense in expenses_db.find(): + expense['_id'] = str(expense['_id']) + history['expenses'].append(expense) + + # sort all by timestamp + history['donations'] = sorted(history['donations'], key=lambda x: x['timestamp']) + + return history + +manager = FinanceManager() + +if __name__ == '__main__': + print(asyncio.run(manager.get_entire_financial_history())) diff --git a/api/handler.py b/api/handler.py index 5c988d5..a4a9aa0 100644 --- a/api/handler.py +++ b/api/handler.py @@ -19,7 +19,7 @@ from helpers import tokens, errors, network load_dotenv() users = UserManager() -models_list = json.load(open('models.json', encoding='utf8')) +models_list = json.load(open('cache/models.json', encoding='utf8')) models = [model['id'] for model in models_list['data']] with open('config/config.yml', encoding='utf8') as f: