diff --git a/django_etebase/migrations/0037_simplemessage.py b/django_etebase/migrations/0037_simplemessage.py new file mode 100644 index 0000000..18f9718 --- /dev/null +++ b/django_etebase/migrations/0037_simplemessage.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.1 on 2020-12-24 13:03 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('django_etebase', '0036_auto_20201214_1128'), + ] + + operations = [ + migrations.CreateModel( + name='SimpleMessage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uid', models.CharField(db_index=True, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')])), + ('version', models.PositiveSmallIntegerField(default=1)), + ('content', models.BinaryField()), + ('fromUser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outgoing_simple_messages', to=settings.AUTH_USER_MODEL)), + ('toUser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_simple_messages', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/django_etebase/models.py b/django_etebase/models.py index 3060fa4..a81ce4e 100644 --- a/django_etebase/models.py +++ b/django_etebase/models.py @@ -236,6 +236,26 @@ class CollectionInvitation(models.Model): return self.fromMember.collection +class SimpleMessage(models.Model): + uid = models.CharField( + db_index=True, unique=True, blank=False, null=False, max_length=43, validators=[UidValidator] + ) + version = models.PositiveSmallIntegerField(default=1) + + fromUser = models.ForeignKey( + settings.AUTH_USER_MODEL, related_name="outgoing_simple_messages", on_delete=models.CASCADE + ) + toUser = models.ForeignKey( + settings.AUTH_USER_MODEL, related_name="incoming_simple_messages", on_delete=models.CASCADE + ) + content = models.BinaryField(editable=False, blank=False, null=False) + + objects: models.BaseManager["SimpleMessage"] + + def __str__(self): + return "{} {} -> {}".format(self.uid, self.fromUser, self.toUser) + + class UserInfo(models.Model): owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True) version = models.PositiveSmallIntegerField(default=1) diff --git a/etebase_fastapi/main.py b/etebase_fastapi/main.py index 8e8469c..c23d0f4 100644 --- a/etebase_fastapi/main.py +++ b/etebase_fastapi/main.py @@ -11,6 +11,7 @@ from .routers.authentication import authentication_router from .routers.collection import collection_router, item_router from .routers.member import member_router from .routers.invitation import invitation_incoming_router, invitation_outgoing_router +from .routers.simplemessage import simplemessage_router def create_application(prefix="", middlewares=[]): @@ -36,6 +37,7 @@ def create_application(prefix="", middlewares=[]): app.include_router( invitation_outgoing_router, prefix=f"{BASE_PATH}/invitation/outgoing", tags=["outgoing invitation"] ) + app.include_router(simplemessage_router, prefix=f"{BASE_PATH}/simplemessage", tags=["simple message"]) if settings.DEBUG: from etebase_fastapi.routers.test_reset_view import test_reset_view_router diff --git a/etebase_fastapi/routers/simplemessage.py b/etebase_fastapi/routers/simplemessage.py new file mode 100644 index 0000000..8ef434b --- /dev/null +++ b/etebase_fastapi/routers/simplemessage.py @@ -0,0 +1,145 @@ +import typing as t + +from django.db import transaction, IntegrityError +from django.db.models import QuerySet +from fastapi import APIRouter, Depends, status, Request + +from django_etebase import models +from django_etebase.utils import get_user_queryset, CallbackContext +from myauth.models import UserType, get_typed_user_model +from .authentication import get_authenticated_user +from ..msgpack import MsgpackRoute +from ..exceptions import HttpError +from ..utils import ( + get_object_or_404, + Context, + BaseModel, + permission_responses, + PERMISSIONS_READ, + PERMISSIONS_READWRITE, +) + +User = get_typed_user_model() +simplemessage_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) +SimpleMessageQuerySet = QuerySet[models.SimpleMessage] +default_queryset: SimpleMessageQuerySet = models.SimpleMessage.objects.all() + + +def get_queryset(user: UserType = Depends(get_authenticated_user)) -> SimpleMessageQuerySet: + return default_queryset.filter(toUser=user) + + +class SimpleMessageCommon(BaseModel): + uid: str + version: int + toUsername: str + content: bytes + + +class SimpleMessageIn(SimpleMessageCommon): + def validate_db(self, context: Context): + user = context.user + if user is not None and (user.username == self.toUsername.lower()): + raise HttpError("no_self_invite", "Inviting yourself is not allowed") + + +class SimpleMessageOut(SimpleMessageCommon): + fromUsername: str + fromPubkey: bytes + + class Config: + orm_mode = True + + @classmethod + def from_orm(cls: t.Type["SimpleMessageOut"], obj: models.SimpleMessage) -> "SimpleMessageOut": + return cls( + uid=obj.uid, + version=obj.version, + toUsername=obj.toUser.username, + fromUsername=obj.fromUser.username, + fromPubkey=bytes(obj.fromUser.userinfo.pubkey), + content=bytes(obj.content), + ) + + +class SimpleMessageListResponse(BaseModel): + data: t.List[SimpleMessageOut] + iterator: t.Optional[str] + done: bool + + +@simplemessage_router.get( + "/", + response_model=SimpleMessageListResponse, + dependencies=[Depends(get_authenticated_user), *PERMISSIONS_READ], +) +def simplemessage_list( + iterator: t.Optional[str] = None, + limit: int = 50, + queryset: SimpleMessageQuerySet = Depends(get_queryset), +): + queryset = queryset.order_by("id") + + if iterator is not None: + iterator_obj = get_object_or_404(queryset, uid=iterator) + queryset = queryset.filter(id__lt=iterator_obj.id) + + result = list(queryset[: limit + 1]) + if len(result) < limit + 1: + done = True + else: + done = False + result = result[:-1] + + ret_data = [SimpleMessageOut.from_orm(revision) for revision in result] + iterator = ret_data[-1].uid if len(result) > 0 else None + + return SimpleMessageListResponse( + data=ret_data, + iterator=iterator, + done=done, + ) + + +@simplemessage_router.get( + "/{message_uid}/", + response_model=SimpleMessageListResponse, + dependencies=PERMISSIONS_READ, +) +def simplemessage_get( + message_uid: str, + queryset: SimpleMessageQuerySet = Depends(get_queryset), +): + obj = get_object_or_404(queryset, uid=message_uid) + return SimpleMessageOut.from_orm(obj) + + +@simplemessage_router.delete( + "/{message_uid}/", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=PERMISSIONS_READWRITE, +) +def simplemessage_delete( + message_uid: str, + queryset: SimpleMessageQuerySet = Depends(get_queryset), +): + obj = get_object_or_404(queryset, uid=message_uid) + obj.delete() + + +@simplemessage_router.post("/", status_code=status.HTTP_201_CREATED, dependencies=PERMISSIONS_READWRITE) +def simplemessage_create( + data: SimpleMessageIn, + request: Request, + user: UserType = Depends(get_authenticated_user), +): + to_user = get_object_or_404( + get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), username=data.toUsername + ) + with transaction.atomic(): + try: + models.SimpleMessage.objects.create(**data.dict(exclude={"toUsername"}), toUser=to_user, fromUser=user) + except IntegrityError: + raise HttpError( + "unique_uid", "SimpleMessage with this uid already exists", status_code=status.HTTP_409_CONFLICT + ) diff --git a/etebase_fastapi/routers/test_reset_view.py b/etebase_fastapi/routers/test_reset_view.py index 09638e4..60fa777 100644 --- a/etebase_fastapi/routers/test_reset_view.py +++ b/etebase_fastapi/routers/test_reset_view.py @@ -34,5 +34,6 @@ def reset(data: SignupIn, request: Request): user.collection_set.all().delete() user.collectionmember_set.all().delete() user.incoming_invitations.all().delete() + user.incoming_simple_messages.all().delete() # FIXME: also delete chunk files!!!