nova-api/api/core.py

205 lines
6.1 KiB
Python
Raw Normal View History

2023-08-01 02:38:55 +02:00
"""User management."""
import os
import sys
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.append(project_root)
# the code above is to allow importing from the root folder
2023-09-22 00:30:07 +02:00
import time
2023-08-01 02:38:55 +02:00
import json
2023-08-21 21:09:22 +02:00
import hmac
2023-09-22 00:30:07 +02:00
import httpx
2023-08-01 02:38:55 +02:00
import fastapi
2023-10-06 09:37:16 +02:00
import aiofiles
2023-08-01 02:38:55 +02:00
2023-08-04 03:30:56 +02:00
from dhooks import Webhook, Embed
2023-08-01 02:38:55 +02:00
from dotenv import load_dotenv
import checks.client
from helpers import errors
2023-09-22 00:30:07 +02:00
from db import users, finances
2023-08-01 02:38:55 +02:00
load_dotenv()
router = fastapi.APIRouter(tags=['core'])
async def check_core_auth(request):
"""Checks the core API key. Returns nothing if it's valid, otherwise returns an error.
2023-08-13 11:16:23 +02:00
"""
2023-08-01 02:38:55 +02:00
received_auth = request.headers.get('Authorization')
2023-08-21 21:09:22 +02:00
correct_core_api = os.environ['CORE_API_KEY']
# use hmac.compare_digest to prevent timing attacks
if not (received_auth and hmac.compare_digest(received_auth, correct_core_api)):
return await errors.error(401, 'The core API key you provided is invalid.', 'Check the `Authorization` header.')
return None
2023-08-01 02:38:55 +02:00
async def new_user_webhook(user: dict) -> None:
"""Runs when a new user is created."""
dhook = Webhook(os.environ['DISCORD_WEBHOOK__USER_CREATED'])
2023-08-04 03:30:56 +02:00
embed = Embed(
description='New User',
color=0x90ee90,
)
2023-08-04 03:30:56 +02:00
dc = user['auth']['discord']
2023-08-06 21:42:07 +02:00
embed.add_field(name='ID', value=str(user['_id']), inline=False)
embed.add_field(name='Discord', value=dc or '-')
embed.add_field(name='Github', value=user['auth']['github'] or '-')
2023-08-04 03:30:56 +02:00
dhook.send(content=f'<@{dc}>', embed=embed)
2023-08-04 03:30:56 +02:00
2023-09-17 21:41:16 +02:00
@router.get('/users')
async def get_users(discord_id: int, incoming_request: fastapi.Request):
"""Returns a user by their discord ID. Requires a core API key."""
auth = await check_core_auth(incoming_request)
if auth: return auth
2023-09-22 00:30:07 +02:00
user = await users.manager.user_by_discord_id(discord_id)
2023-09-17 21:41:16 +02:00
if not user:
return await errors.error(404, 'Discord user not found in the API database.', 'Check the `discord_id` parameter.')
# turn the ObjectId into a string
user['_id'] = str(user['_id'])
return user
2023-08-01 02:38:55 +02:00
@router.post('/users')
async def create_user(incoming_request: fastapi.Request):
"""Creates a user. Requires a core API key."""
2023-08-01 02:38:55 +02:00
auth_error = await check_core_auth(incoming_request)
if auth_error:
return auth_error
try:
payload = await incoming_request.json()
discord_id = payload.get('discord_id')
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.')
2023-09-22 00:30:07 +02:00
user = await users.manager.create(discord_id)
2023-08-06 21:42:07 +02:00
await new_user_webhook(user)
2023-08-04 03:30:56 +02:00
2023-09-14 20:43:24 +02:00
user['_id'] = str(user['_id'])
2023-08-01 02:38:55 +02:00
return user
2023-08-18 21:23:00 +02:00
@router.put('/users')
async def update_user(incoming_request: fastapi.Request):
"""Updates a user. Requires a core API key."""
2023-08-18 21:23:00 +02:00
auth_error = await check_core_auth(incoming_request)
2023-09-17 21:41:16 +02:00
if auth_error: return auth_error
2023-08-18 21:23:00 +02:00
try:
payload = await incoming_request.json()
discord_id = payload.get('discord_id')
updates = payload.get('updates')
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 and an `updates` key.'
)
2023-09-22 00:30:07 +02:00
user = await users.manager.update_by_discord_id(discord_id, updates)
2023-08-18 21:23:00 +02:00
return user
@router.get('/checks')
async def run_checks(incoming_request: fastapi.Request):
"""Tests the API. Requires a core API key."""
auth_error = await check_core_auth(incoming_request)
2023-09-17 21:41:16 +02:00
if auth_error: return auth_error
2023-09-14 18:18:19 +02:00
results = {}
funcs = [
checks.client.test_chat_non_stream_gpt4,
checks.client.test_chat_stream_gpt3,
checks.client.test_function_calling,
2023-10-09 19:09:01 +02:00
# checks.client.test_image_generation,
2023-09-14 22:45:57 +02:00
# checks.client.test_speech_to_text,
2023-09-14 18:18:19 +02:00
checks.client.test_models
]
for func in funcs:
try:
result = await func()
except Exception as exc:
results[func.__name__] = str(exc)
else:
results[func.__name__] = result
return results
2023-09-22 00:30:07 +02:00
async def get_crypto_price(cryptocurrency: str) -> float:
"""Gets the price of a cryptocurrency using coinbase's API."""
2023-10-06 09:37:16 +02:00
cache_path = os.path.join('cache', 'crypto_prices.json')
try:
async with aiofiles.open(cache_path) as f:
content = await f.read()
except FileNotFoundError:
2023-09-22 00:30:07 +02:00
cache = {}
2023-10-06 09:37:16 +02:00
else:
cache = json.loads(content)
2023-09-22 00:30:07 +02:00
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')
2023-10-12 00:03:15 +02:00
response.raise_for_status()
2023-09-22 00:30:07 +02:00
usd_price = float(response.json()['data']['amount'])
cache[cryptocurrency] = usd_price
cache['_last_updated'] = time.time()
2023-10-06 09:37:16 +02:00
async with aiofiles.open(cache_path, 'w') as f:
for chunk in json.JSONEncoder().iterencode(cache):
await f.write(chunk)
2023-09-22 00:30:07 +02:00
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
transactions['timestamp'] = time.time()
2023-09-22 00:30:07 +02:00
return transactions