diff --git a/server/README.md b/server/README.md index 07475a38..6e45d8e5 100644 --- a/server/README.md +++ b/server/README.md @@ -10,3 +10,13 @@ A translation ID is a hash created using the following information : - the destination language - the source text - the destination text + +## Environment Variables + +| Name | Description | Default  | +|---------------------------|------------------------------------------------------------------------|-------------| +| `HOST` | The host to listen to | `127.0.0.1` | +| `PORT` | The port to listen to | `5001` | +| `TRANSLATEPY_DB_DISABLED` | To disable any DB interaction | `False`  | +| `TRANSLATEPY_MONGO_URI` | The MongoDB URI to connect to. If none, a `mongod` process will be ran | `None`  | +| `TRANSLATEPY_IP_SALT` | The salt to create IP hashes | ``  | diff --git a/server/db.py b/server/db.py index 8234cc7f..e0c45b61 100644 --- a/server/db.py +++ b/server/db.py @@ -1,11 +1,13 @@ from os import environ -from nasse.logging import log, LogLevels + +from nasse.logging import LogLevels, log from nasse.utils.boolean import to_bool from yuno import MongoDB + from schemas.client import TranslatepyClient if not to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): - MONGO_URI = environ.get("MONGO_URI", None) + MONGO_URI = environ.get("TRANSLATEPY_MONGO_URI", None) if MONGO_URI is None: mongo = MongoDB() log("Starting MongoDB", LogLevels.INFO) @@ -14,4 +16,4 @@ client = TranslatepyClient(MONGO_URI, connect=False) else: - client = {} + client = TranslatepyClient() diff --git a/server/endpoints/stars.py b/server/endpoints/stars.py index e69de29b..07f07559 100644 --- a/server/endpoints/stars.py +++ b/server/endpoints/stars.py @@ -0,0 +1,145 @@ +from os import environ + +from db import client +from exceptions import DatabaseDisabled, Forbidden, NotFound +from nasse import Response, Request +from nasse.models import Dynamic, Endpoint, Error, Login, Param, Return +from nasse.utils.boolean import to_bool +from translatepy.server.server import app + +from datetime import datetime + +from yuno.security.hash import Hasher +from yuno.security.token import TokenManager +from yuno.security.encrypt import AES + +hasher = Hasher() +aes = AES(client, prefix="translatepy") +token_manager = TokenManager(key=client, sign=client) + +base = Endpoint( + section="Stars", + errors=Error(name="DATABASE_DISABLED", description="When the server disabled any database interaction", code=501), + login=Login(no_login=True) +) + + +if not to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): + stars = client.translatepy.stars +else: + stars = {} + + +@app.route("/stars", description="Get all starred translations") +def stars_handler(request: Request): + if to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): + raise DatabaseDisabled + + query = stars.find({ + "users.{hash}".format( + hash=hasher.hash_string( + "{ip}{salt}".format( + ip=request.client_ip, + salt=environ.get("TRANSLATEPY_SALT", "") + ) + ) + ): { + "$exists": True + } + }) + results = [] + for document in query: + result = document.copy() + result["users"] = len(result["users"]) + results.append(result) + return Response( + data={ + "stars": results + }, + message="Here are your starred translations" + ) + + +def TranslationToken(value: str): + return token_manager.decode(value, encryption=aes) + + +@app.route("/stars/", Endpoint( + methods=["GET", "POST", "DELETE"], + description={ + "GET": "Get the stars for a translation", + "POST": "Star a translation", + "DELETE": "Unstar a translation" + }, + params=[ + Param(name="token", description="The token to authenticate the translation", type=TranslationToken, methods="POST") + ], + dynamics=[ + Dynamic(name="translation_id", description="The ID of the translation to star", methods="POST"), + Dynamic(name="translation_id", description="The ID of the translation to get", methods="GET"), + Dynamic(name="translation_id", description="The ID of the translation to unstar", methods="DELETE"), + ], + errors=[ + Error(name="FORBIDDEN", description="You are not allowed to star this translation", code=403), + Error(name="NOT_FOUND", description="The translation could not be found", code=404) + ] + base.errors, + returns=[ + Return(name="source", description="The source text", methods=["GET", "POST"]), + Return(name="result", description="The result text", methods=["GET", "POST"]), + Return(name="language", description="The translation languages", children=[ + Return(name="source", description="The source language", methods=["GET", "POST"]), + Return(name="dest", description="The destination language", methods=["GET", "POST"]) + ], methods=["GET", "POST"]), + Return(name="users", description="The number of users who starred the translation", type=int, methods=["GET", "POST"]) + ] +)) +def stars__translation_id__(request: Request, method: str, translation_id: str, token: dict = None): + if to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): + raise DatabaseDisabled + + current_ip_hash = hasher.hash_string( + "{ip}{salt}".format( + ip=request.client_ip, + salt=environ.get("TRANSLATEPY_SALT", "") + ) + ) + + if method == "DELETE": + stars.update({ + "_id": translation_id # query + }, { + "$unset": { # command + "users.{hash}".format(hash=current_ip_hash): "" + } + }) + return "Removed the star" + + try: + translation = stars[translation_id] + except KeyError as err: + raise NotFound("We couldn't find the given translation") from err + + if method == "POST": + # token body + # { + # "sub": "user hash", + # "source": "source text", + # "result": "result text", + # "language": { + # "source": "source language", + # "dest": "destination language" + # } + # } + if current_ip_hash != token["sub"]: + raise Forbidden("You are not allowed to star this translation") + + if len(translation.users) <= 0: + translation.source = token["source"] + translation.result = token["result"] + translation.language = token["language"] + + translation.users[current_ip_hash] = datetime.utcnow() + + result = translation.copy() + result["users"] = len(result["users"]) + return Response(result) diff --git a/server/endpoints/translation.py b/server/endpoints/translation.py index 2c22ead3..5abe2335 100644 --- a/server/endpoints/translation.py +++ b/server/endpoints/translation.py @@ -5,6 +5,7 @@ from nasse.timer import Timer from db import client from nasse.utils.boolean import to_bool +from server.exceptions import DatabaseDisabled import translatepy from translatepy.server.translation import (BaseTranslator, FlaskResponse, Language, List, NoResult, Queue, @@ -14,7 +15,9 @@ def log_time(service: BaseTranslator, time: float, storage: dict): - print("{service} took {time} seconds".format(service=service, time=time)) + # print("{service} took {time} seconds".format(service=service, time=time)) + if to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): + raise DatabaseDisabled storage[str(service).replace(".", "*dot*")] = time @@ -26,7 +29,9 @@ def log_time(service: BaseTranslator, time: float, storage: dict): def log_error(service: str, error: str): - print("{service} failed: {error}".format(service=service, error=error)) + # print("{service} failed: {error}".format(service=service, error=error)) + if to_bool(environ.get("TRANSLATEPY_DB_DISABLED", False)): + raise DatabaseDisabled new_id = ObjectId() errors[new_id] = { "_id": new_id, diff --git a/server/exceptions.py b/server/exceptions.py new file mode 100644 index 00000000..d2f39495 --- /dev/null +++ b/server/exceptions.py @@ -0,0 +1,22 @@ +from nasse.exceptions import NasseException + + +class DatabaseDisabled(NasseException): + STATUS_CODE = 501 + MESSAGE = "The database is currently disabled on the server" + EXCEPTION_NAME = "DATABASE_DISABLED" + LOG = False + + +class Forbidden(NasseException): + STATUS_CODE = 403 + MESSAGE = "You do not have the rights to access this endpoint" + EXCEPTION_NAME = "FORBIDDEN" + LOG = False + + +class NotFound(NasseException): + STATUS_CODE = 404 + MESSAGE = "We couldn't find the resource you were looking for" + EXCEPTION_NAME = "NOT_FOUND" + LOG = False diff --git a/server/schemas/client.py b/server/schemas/client.py index a0b98900..ab1f5153 100644 --- a/server/schemas/client.py +++ b/server/schemas/client.py @@ -1,5 +1,6 @@ from yuno import YunoClient, YunoDatabase +from schemas.stars import StarsCollection from schemas.errors import ErrorsCollection from schemas.timings import TimingsCollection @@ -7,6 +8,7 @@ class TranslatepyDatabase(YunoDatabase): errors: ErrorsCollection timings: TimingsCollection + stars: StarsCollection class TranslatepyClient(YunoClient): diff --git a/server/schemas/errors.py b/server/schemas/errors.py index 54c91aa0..90619771 100644 --- a/server/schemas/errors.py +++ b/server/schemas/errors.py @@ -1,4 +1,4 @@ -from datetime import datetime +from schemas.types import Datetime from yuno import YunoDict, YunoCollection @@ -8,7 +8,7 @@ class Error(YunoDict): """ service: str error: str - timestamp: datetime + timestamp: Datetime class ErrorsCollection(YunoCollection): diff --git a/server/schemas/stars.py b/server/schemas/stars.py index a7394d50..3aab9123 100644 --- a/server/schemas/stars.py +++ b/server/schemas/stars.py @@ -1,13 +1,21 @@ -from datetime import datetime +from schemas.types import Datetime from yuno import YunoDict, YunoCollection +class StarredTranslationLanguage(YunoDict): + """Starred translation language""" + source: str + dest: str + + class StarredTranslation(YunoDict): """User starred translations""" _id: str # translation ID - timestamp: datetime - services: list[str] - users: list[str] + language: StarredTranslationLanguage = {} + source: str = "" # source text + result: str = "" # translated text + services: list[str] = [] + users: dict[str, Datetime] class StarsCollection(YunoCollection): diff --git a/server/schemas/timings.py b/server/schemas/timings.py index 4b582a67..156d8a88 100644 --- a/server/schemas/timings.py +++ b/server/schemas/timings.py @@ -1,16 +1,8 @@ from bson import ObjectId -from datetime import datetime +from schemas.types import Datetime from yuno import YunoDict, YunoCollection -class Datetime(datetime): - def __new__(self, *args, **kwargs) -> datetime: - print(args, kwargs) - if len(args) > 0 and isinstance(args[0], datetime): - return args[0] - return datetime(*args, **kwargs) - - class ServiceTimings(YunoDict): _id: ObjectId timings: dict[str, int] diff --git a/server/schemas/types.py b/server/schemas/types.py new file mode 100644 index 00000000..fd6ed777 --- /dev/null +++ b/server/schemas/types.py @@ -0,0 +1,9 @@ +from datetime import datetime + + +class Datetime(datetime): + def __new__(self, *args, **kwargs) -> datetime: + print(args, kwargs) + if len(args) > 0 and isinstance(args[0], datetime): + return args[0] + return datetime(*args, **kwargs) diff --git a/translatepy/server/language.py b/translatepy/server/language.py index 0b07583f..74f2ac26 100644 --- a/translatepy/server/language.py +++ b/translatepy/server/language.py @@ -1,5 +1,5 @@ from nasse import Response -from nasse.models import Dynamic, Endpoint, Error, Login, Param, Return, Header +from nasse.models import Dynamic, Endpoint, Error, Login, Param, Return from nasse.utils.boolean import to_bool import translatepy from translatepy.exceptions import UnknownLanguage diff --git a/translatepy/server/translation.py b/translatepy/server/translation.py index be92cf08..027f140b 100644 --- a/translatepy/server/translation.py +++ b/translatepy/server/translation.py @@ -6,7 +6,7 @@ from bs4 import NavigableString from flask import Response as FlaskResponse from nasse import Response -from nasse.models import Endpoint, Error, Login, Param, Return, Header +from nasse.models import Endpoint, Error, Login, Param, Return import translatepy from translatepy import Translator from translatepy.exceptions import NoResult, UnknownLanguage, UnknownTranslator