diff --git a/src/aleph/web/controllers/prices.py b/src/aleph/web/controllers/prices.py index 4fef62052..625c2ed59 100644 --- a/src/aleph/web/controllers/prices.py +++ b/src/aleph/web/controllers/prices.py @@ -1,47 +1,85 @@ import logging +from dataclasses import dataclass from decimal import Decimal from typing import Optional from aiohttp import web -from dataclasses import dataclass +from aiohttp.web_exceptions import HTTPException +from aleph_message.models import ExecutableContent, ItemHash, MessageType from dataclasses_json import DataClassJsonMixin -from aleph_message.models import ItemHash, MessageType -from aleph.db.accessors.messages import ( - get_message_status, get_message_by_item_hash, -) +from aleph.db.accessors.messages import get_message_by_item_hash, get_message_status +from aleph.db.models import MessageDb, MessageStatusDb from aleph.services.cost import compute_cost, compute_flow_cost -from aleph.types.db_session import DbSessionFactory +from aleph.types.db_session import DbSession, DbSessionFactory LOGGER = logging.getLogger(__name__) +# This is not defined in aiohttp.web_exceptions +class HTTPProcessing(HTTPException): + status_code = 102 + + +# Mapping between message statuses to their corresponding exceptions and messages +MESSAGE_STATUS_EXCEPTIONS = { + MessageStatusDb.statuses.PENDING: (HTTPProcessing, "Message still pending"), + MessageStatusDb.statuses.REJECTED: (web.HTTPNotFound, "This message was rejected"), + MessageStatusDb.statuses.FORGOTTEN: ( + web.HTTPGone, + "This message has been forgotten", + ), +} + + @dataclass class MessagePrice(DataClassJsonMixin): """Dataclass used to expose message required tokens.""" + required_tokens: Optional[Decimal] = None -async def message_price(request: web.Request): - item_hash_str = request.match_info.get("item_hash") +async def get_executable_message(session: DbSession, item_hash_str: str) -> MessageDb: + """Attempt to get an executable message from the database. + Raises an HTTP exception if the message is not found, not processed or is not an executable message. + """ + + # Parse the item_hash_str into an ItemHash object try: item_hash = ItemHash(item_hash_str) except ValueError: - raise web.HTTPUnprocessableEntity(body=f"Invalid message hash: {item_hash_str}") + raise web.HTTPBadRequest(body=f"Invalid message hash: {item_hash_str}") + + # Get the message status from the database + message_status_db = get_message_status(session=session, item_hash=item_hash) + if not message_status_db: + raise web.HTTPNotFound() + # Loop through the status_exceptions to find a match and raise the corresponding exception + if message_status_db.status in MESSAGE_STATUS_EXCEPTIONS: + exception, message = MESSAGE_STATUS_EXCEPTIONS[message_status_db.status] + raise exception(body=f"{message}: {item_hash_str}") + assert message_status_db.status == MessageStatusDb.statuses.PROCESSED + + # Get the message from the database + message = get_message_by_item_hash(session, item_hash) + if not message: + raise web.HTTPNotFound(body="Message not found, despite appearing as processed") + if message.type not in (MessageType.instance, MessageType.program): + raise web.HTTPBadRequest( + body=f"Message is not an executable message: {item_hash_str}" + ) + + return message + + +async def message_price(request: web.Request): + """Returns the price of an executable message.""" session_factory: DbSessionFactory = request.app["session_factory"] with session_factory() as session: - message_status_db = get_message_status(session=session, item_hash=item_hash) - if message_status_db is None: - raise web.HTTPNotFound() - message = get_message_by_item_hash(session, item_hash) - - if not message or message.type != (MessageType.instance or MessageType.program): - raise web.HTTPUnprocessableEntity( - body=f"Invalid message hash: {item_hash_str}" - ) + message = await get_executable_message(session, request.match_info["item_hash"]) - content = message.parsed_content + content: ExecutableContent = message.parsed_content if content.payment and content.payment.is_stream: required_tokens = compute_flow_cost(session=session, content=content)