Compare commits

..

4 commits

Author SHA1 Message Date
henceiusegentoo d3d9ead8f4 Added /v1/accounts/credits 2023-09-22 02:38:21 +02:00
nsde 1384b3add8 Added finances endpoint 2023-09-22 00:30:07 +02:00
henceiusegentoo 453c6e7430 Removed local from backups 2023-09-21 22:38:12 +02:00
henceiusegentoo 4673b23055 Made the API return 404 if the model can't be found 2023-09-21 20:17:32 +02:00
9 changed files with 129 additions and 30 deletions

View file

@ -4,7 +4,7 @@
# git commit -am "Auto-trigger - Production server started" && git push origin Production # git commit -am "Auto-trigger - Production server started" && git push origin Production
# backup database # backup database
/usr/local/bin/python /home/nova-api/backup_manager/main.py pre_prodpush /usr/local/bin/python /home/nova-api/api/backup_manager/main.py pre_prodpush
# Kill production server # Kill production server
fuser -k 2333/tcp fuser -k 2333/tcp

View file

@ -1,21 +1,22 @@
from dotenv import load_dotenv
import os import os
from motor.motor_asyncio import AsyncIOMotorClient
from bson import json_util
import json import json
from sys import argv
import asyncio import asyncio
from sys import argv
from bson import json_util
from dotenv import load_dotenv
from motor.motor_asyncio import AsyncIOMotorClient
load_dotenv() load_dotenv()
MONGO_URI = os.getenv("MONGO_URI") MONGO_URI = os.environ['MONGO_URI']
FILE_DIR = os.path.dirname(os.path.realpath(__file__)) FILE_DIR = os.path.dirname(os.path.realpath(__file__))
async def main(output_dir): async def main(output_dir: str):
await make_backup(output_dir) await make_backup(output_dir)
async def make_backup(output_dir): async def make_backup(output_dir: str):
output_dir = os.path.join(FILE_DIR, "..", "backups", output_dir) output_dir = os.path.join(FILE_DIR, '..', 'backups', output_dir)
if not os.path.exists(output_dir): if not os.path.exists(output_dir):
os.makedirs(output_dir) os.makedirs(output_dir)
@ -25,26 +26,29 @@ async def make_backup(output_dir):
databases = {db: await client[db].list_collection_names() for db in databases} databases = {db: await client[db].list_collection_names() for db in databases}
for database in databases: for database in databases:
if not os.path.exists(f"{output_dir}/{database}"): if database == 'local':
os.mkdir(f"{output_dir}/{database}") continue
if not os.path.exists(f'{output_dir}/{database}'):
os.mkdir(f'{output_dir}/{database}')
for collection in databases[database]: for collection in databases[database]:
print(f"Making backup for {database}/{collection}") print(f'Making backup for {database}/{collection}')
await make_backup_for_collection(database, collection, output_dir) await make_backup_for_collection(database, collection, output_dir)
async def make_backup_for_collection(database, collection, output_dir): async def make_backup_for_collection(database, collection, output_dir):
path = f"{output_dir}/{database}/{collection}.json" path = f'{output_dir}/{database}/{collection}.json'
client = AsyncIOMotorClient(MONGO_URI) client = AsyncIOMotorClient(MONGO_URI)
collection = client[database][collection] collection = client[database][collection]
documents = await collection.find({}).to_list(length=None) documents = await collection.find({}).to_list(length=None)
with open(path, "w") as f: with open(path, 'w') as f:
json.dump(documents, f, default=json_util.default) json.dump(documents, f, default=json_util.default)
if __name__ == "__main__": if __name__ == '__main__':
if len(argv) < 2 or len(argv) > 2: if len(argv) < 2 or len(argv) > 2:
print("Usage: python3 main.py <output_dir>") print('Usage: python3 main.py <output_dir>')
exit(1) exit(1)
output_dir = argv[1] output_dir = argv[1]

1
api/cache/crypto_prices.json vendored Normal file
View file

@ -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}

View file

@ -8,9 +8,12 @@ sys.path.append(project_root)
# the code above is to allow importing from the root folder # the code above is to allow importing from the root folder
import time
import json import json
import hmac import hmac
import httpx
import fastapi import fastapi
import functools
from dhooks import Webhook, Embed from dhooks import Webhook, Embed
from dotenv import load_dotenv from dotenv import load_dotenv
@ -18,7 +21,7 @@ from dotenv import load_dotenv
import checks.client import checks.client
from helpers import errors from helpers import errors
from db.users import UserManager from db import users, finances
load_dotenv() load_dotenv()
router = fastapi.APIRouter(tags=['core']) 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) auth = await check_core_auth(incoming_request)
if auth: return auth if auth: return auth
# Get user by discord ID user = await users.manager.user_by_discord_id(discord_id)
manager = UserManager()
user = await manager.user_by_discord_id(discord_id)
if not user: if not user:
return await errors.error(404, 'Discord user not found in the API database.', 'Check the `discord_id` parameter.') 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): 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.') 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 user = await users.manager.create(discord_id)
manager = UserManager()
user = await manager.create(discord_id)
await new_user_webhook(user) await new_user_webhook(user)
user['_id'] = str(user['_id']) 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.' 'The payload must be a JSON object with a `discord_id` key and an `updates` key.'
) )
# Update the user user = await users.manager.update_by_discord_id(discord_id, updates)
manager = UserManager()
user = await manager.update_by_discord_id(discord_id, updates)
return user return user
@ -147,3 +144,54 @@ async def run_checks(incoming_request: fastapi.Request):
results[func.__name__] = result results[func.__name__] = result
return results 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

39
api/db/finances.py Normal file
View file

@ -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()))

View file

@ -19,7 +19,8 @@ from helpers import tokens, errors, network
load_dotenv() load_dotenv()
users = UserManager() 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: with open('config/config.yml', encoding='utf8') as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
@ -70,6 +71,9 @@ async def handle(incoming_request: fastapi.Request):
if ban_reason: if ban_reason:
return await errors.error(403, f'Your NovaAI account has been banned. Reason: \'{ban_reason}\'.', 'Contact the staff for an appeal.') return await errors.error(403, f'Your NovaAI account has been banned. Reason: \'{ban_reason}\'.', 'Contact the staff for an appeal.')
if 'account/credits' in path:
return fastapi.responses.JSONResponse({'credits': user['credits']})
costs = config['costs'] costs = config['costs']
cost = costs['other'] cost = costs['other']
@ -149,6 +153,9 @@ async def handle(incoming_request: fastapi.Request):
media_type = 'text/event-stream' if payload.get('stream', False) else 'application/json' media_type = 'text/event-stream' if payload.get('stream', False) else 'application/json'
if (model := payload.get('model')) not in models and model is not None:
return await errors.error(404, 'Model not found.', 'Check the model name and try again.')
return fastapi.responses.StreamingResponse( return fastapi.responses.StreamingResponse(
content=responder.respond( content=responder.respond(
user=user, user=user,

View file

@ -66,4 +66,4 @@ async def root():
@app.route('/v1/{path:path}', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @app.route('/v1/{path:path}', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
async def v1_handler(request: fastapi.Request): async def v1_handler(request: fastapi.Request):
res = await handler.handle(incoming_request=request) res = await handler.handle(incoming_request=request)
return res return res

View file

@ -217,8 +217,8 @@ async def demo():
print('Checking streamed chat completions...') print('Checking streamed chat completions...')
print(await test_chat_stream_gpt3()) print(await test_chat_stream_gpt3())
print('[lightblue]Checking if image generation works...') # print('[lightblue]Checking if image generation works...')
print(await test_image_generation()) # print(await test_image_generation())
print('Checking the models endpoint...') print('Checking the models endpoint...')
print(await test_models()) print(await test_models())