Posting and fan-out both working
This commit is contained in:
		
							parent
							
								
									feb5d9b74f
								
							
						
					
					
						commit
						8fd5a9292c
					
				@ -1,11 +1,11 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from activities.models import Post, TimelineEvent
 | 
			
		||||
from activities.models import FanOut, Post, TimelineEvent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Post)
 | 
			
		||||
class PostAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["id", "author", "created"]
 | 
			
		||||
    list_display = ["id", "state", "author", "created"]
 | 
			
		||||
    raw_id_fields = ["to", "mentions"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,3 +13,9 @@ class PostAdmin(admin.ModelAdmin):
 | 
			
		||||
class TimelineEventAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["id", "identity", "created", "type"]
 | 
			
		||||
    raw_id_fields = ["identity", "subject_post", "subject_identity"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(FanOut)
 | 
			
		||||
class FanOutAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["id", "state", "state_attempted", "type", "identity"]
 | 
			
		||||
    raw_id_fields = ["identity", "subject_post"]
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=False)),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=True)),
 | 
			
		||||
                ("state_changed", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("state_attempted", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("state_locked_until", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										103
									
								
								activities/migrations/0002_fan_out.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								activities/migrations/0002_fan_out.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,103 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-12 05:36
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import activities.models.fan_out
 | 
			
		||||
import stator.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0001_initial"),
 | 
			
		||||
        ("activities", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="authored",
 | 
			
		||||
            field=models.DateTimeField(default=django.utils.timezone.now),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="author",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                on_delete=django.db.models.deletion.PROTECT,
 | 
			
		||||
                related_name="posts",
 | 
			
		||||
                to="users.identity",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="mentions",
 | 
			
		||||
            field=models.ManyToManyField(
 | 
			
		||||
                blank=True, related_name="posts_mentioning", to="users.identity"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="to",
 | 
			
		||||
            field=models.ManyToManyField(
 | 
			
		||||
                blank=True, related_name="posts_to", to="users.identity"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="FanOut",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.BigAutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=True)),
 | 
			
		||||
                ("state_changed", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("state_attempted", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("state_locked_until", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "state",
 | 
			
		||||
                    stator.models.StateField(
 | 
			
		||||
                        choices=[("new", "new"), ("sent", "sent")],
 | 
			
		||||
                        default="new",
 | 
			
		||||
                        graph=activities.models.fan_out.FanOutStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "type",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        choices=[("post", "Post"), ("boost", "Boost")], max_length=100
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "identity",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="fan_outs",
 | 
			
		||||
                        to="users.identity",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "subject_post",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="fan_outs",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,2 +1,3 @@
 | 
			
		||||
from .fan_out import FanOut  # noqa
 | 
			
		||||
from .post import Post  # noqa
 | 
			
		||||
from .timeline_event import TimelineEvent  # noqa
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										81
									
								
								activities/models/fan_out.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								activities/models/fan_out.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
			
		||||
from asgiref.sync import sync_to_async
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from activities.models.timeline_event import TimelineEvent
 | 
			
		||||
from core.ld import canonicalise
 | 
			
		||||
from core.signatures import HttpSignature
 | 
			
		||||
from stator.models import State, StateField, StateGraph, StatorModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FanOutStates(StateGraph):
 | 
			
		||||
    new = State(try_interval=300)
 | 
			
		||||
    sent = State()
 | 
			
		||||
 | 
			
		||||
    new.transitions_to(sent)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    async def handle_new(cls, instance: "FanOut"):
 | 
			
		||||
        """
 | 
			
		||||
        Sends the fan-out to the right inbox.
 | 
			
		||||
        """
 | 
			
		||||
        fan_out = await instance.afetch_full()
 | 
			
		||||
        if fan_out.identity.local:
 | 
			
		||||
            # Make a timeline event directly
 | 
			
		||||
            await sync_to_async(TimelineEvent.add_post)(
 | 
			
		||||
                identity=fan_out.identity,
 | 
			
		||||
                post=fan_out.subject_post,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            # Send it to the remote inbox
 | 
			
		||||
            post = await fan_out.subject_post.afetch_full()
 | 
			
		||||
            # Sign it and send it
 | 
			
		||||
            await HttpSignature.signed_request(
 | 
			
		||||
                uri=fan_out.identity.inbox_uri,
 | 
			
		||||
                body=canonicalise(post.to_create_ap()),
 | 
			
		||||
                identity=post.author,
 | 
			
		||||
            )
 | 
			
		||||
        return cls.sent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FanOut(StatorModel):
 | 
			
		||||
    """
 | 
			
		||||
    An activity that needs to get to an inbox somewhere.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Types(models.TextChoices):
 | 
			
		||||
        post = "post"
 | 
			
		||||
        boost = "boost"
 | 
			
		||||
 | 
			
		||||
    state = StateField(FanOutStates)
 | 
			
		||||
 | 
			
		||||
    # The user this event is targeted at
 | 
			
		||||
    identity = models.ForeignKey(
 | 
			
		||||
        "users.Identity",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name="fan_outs",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # What type of activity it is
 | 
			
		||||
    type = models.CharField(max_length=100, choices=Types.choices)
 | 
			
		||||
 | 
			
		||||
    # Links to the appropriate objects
 | 
			
		||||
    subject_post = models.ForeignKey(
 | 
			
		||||
        "activities.Post",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        related_name="fan_outs",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    updated = models.DateTimeField(auto_now=True)
 | 
			
		||||
 | 
			
		||||
    ### Async helpers ###
 | 
			
		||||
 | 
			
		||||
    async def afetch_full(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a version of the object with all relations pre-loaded
 | 
			
		||||
        """
 | 
			
		||||
        return await FanOut.objects.select_related("identity", "subject_post").aget(
 | 
			
		||||
            pk=self.pk
 | 
			
		||||
        )
 | 
			
		||||
@ -1,8 +1,13 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
import urlman
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
from activities.models.fan_out import FanOut
 | 
			
		||||
from activities.models.timeline_event import TimelineEvent
 | 
			
		||||
from core.html import sanitize_post
 | 
			
		||||
from core.ld import format_date
 | 
			
		||||
from stator.models import State, StateField, StateGraph, StatorModel
 | 
			
		||||
from users.models.follow import Follow
 | 
			
		||||
from users.models.identity import Identity
 | 
			
		||||
@ -19,7 +24,8 @@ class PostStates(StateGraph):
 | 
			
		||||
        """
 | 
			
		||||
        Creates all needed fan-out objects for a new Post.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
        await instance.afan_out()
 | 
			
		||||
        return cls.fanned_out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Post(StatorModel):
 | 
			
		||||
@ -84,11 +90,21 @@ class Post(StatorModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # When the post was originally created (as opposed to when we received it)
 | 
			
		||||
    authored = models.DateTimeField(default=timezone.now)
 | 
			
		||||
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    updated = models.DateTimeField(auto_now=True)
 | 
			
		||||
 | 
			
		||||
    class urls(urlman.Urls):
 | 
			
		||||
        view = "{self.identity.urls.view}posts/{self.id}/"
 | 
			
		||||
        view = "{self.author.urls.view}posts/{self.id}/"
 | 
			
		||||
        object_uri = "{self.author.urls.actor}posts/{self.id}/"
 | 
			
		||||
 | 
			
		||||
        def get_scheme(self, url):
 | 
			
		||||
            return "https"
 | 
			
		||||
 | 
			
		||||
        def get_hostname(self, url):
 | 
			
		||||
            return self.instance.author.domain.uri_domain
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.author} #{self.id}"
 | 
			
		||||
@ -97,6 +113,16 @@ class Post(StatorModel):
 | 
			
		||||
    def safe_content(self):
 | 
			
		||||
        return sanitize_post(self.content)
 | 
			
		||||
 | 
			
		||||
    ### Async helpers ###
 | 
			
		||||
 | 
			
		||||
    async def afetch_full(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a version of the object with all relations pre-loaded
 | 
			
		||||
        """
 | 
			
		||||
        return await Post.objects.select_related("author", "author__domain").aget(
 | 
			
		||||
            pk=self.pk
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    ### Local creation ###
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@ -111,9 +137,57 @@ class Post(StatorModel):
 | 
			
		||||
        post.save()
 | 
			
		||||
        return post
 | 
			
		||||
 | 
			
		||||
    ### ActivityPub (outgoing) ###
 | 
			
		||||
    ### ActivityPub (outbound) ###
 | 
			
		||||
 | 
			
		||||
    ### ActivityPub (incoming) ###
 | 
			
		||||
    async def afan_out(self):
 | 
			
		||||
        """
 | 
			
		||||
        Creates FanOuts for a new post
 | 
			
		||||
        """
 | 
			
		||||
        # Send a copy to all people who follow this user
 | 
			
		||||
        post = await self.afetch_full()
 | 
			
		||||
        async for follow in post.author.inbound_follows.all():
 | 
			
		||||
            await FanOut.objects.acreate(
 | 
			
		||||
                identity_id=follow.source_id,
 | 
			
		||||
                type=FanOut.Types.post,
 | 
			
		||||
                subject_post=post,
 | 
			
		||||
            )
 | 
			
		||||
        # And one for themselves
 | 
			
		||||
        await FanOut.objects.acreate(
 | 
			
		||||
            identity_id=post.author_id,
 | 
			
		||||
            type=FanOut.Types.post,
 | 
			
		||||
            subject_post=post,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def to_ap(self) -> Dict:
 | 
			
		||||
        """
 | 
			
		||||
        Returns the AP JSON for this object
 | 
			
		||||
        """
 | 
			
		||||
        value = {
 | 
			
		||||
            "type": "Note",
 | 
			
		||||
            "id": self.object_uri,
 | 
			
		||||
            "published": format_date(self.created),
 | 
			
		||||
            "attributedTo": self.author.actor_uri,
 | 
			
		||||
            "content": self.safe_content,
 | 
			
		||||
            "to": "as:Public",
 | 
			
		||||
            "as:sensitive": self.sensitive,
 | 
			
		||||
            "url": self.urls.view.full(),  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        if self.summary:
 | 
			
		||||
            value["summary"] = self.summary
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def to_create_ap(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the AP JSON to create this object
 | 
			
		||||
        """
 | 
			
		||||
        return {
 | 
			
		||||
            "type": "Create",
 | 
			
		||||
            "id": self.object_uri + "#create",
 | 
			
		||||
            "actor": self.author.actor_uri,
 | 
			
		||||
            "object": self.to_ap(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    ### ActivityPub (inbound) ###
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def by_ap(cls, data, create=False) -> "Post":
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import urllib.parse as urllib_parse
 | 
			
		||||
from typing import Dict, List, Union
 | 
			
		||||
 | 
			
		||||
@ -273,6 +274,8 @@ schemas = {
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def builtin_document_loader(url: str, options={}):
 | 
			
		||||
    # Get URL without scheme
 | 
			
		||||
@ -324,3 +327,7 @@ def canonicalise(json_data: Dict, include_security: bool = False) -> Dict:
 | 
			
		||||
        json_data["@context"] = context
 | 
			
		||||
 | 
			
		||||
    return jsonld.compact(jsonld.expand(json_data), context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_date(value: datetime.datetime) -> str:
 | 
			
		||||
    return value.strftime(DATETIME_FORMAT)
 | 
			
		||||
 | 
			
		||||
@ -45,8 +45,8 @@ class StatorModel(models.Model):
 | 
			
		||||
    concrete model yourself.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # If this row is up for transition attempts
 | 
			
		||||
    state_ready = models.BooleanField(default=False)
 | 
			
		||||
    # If this row is up for transition attempts (which it always is on creation!)
 | 
			
		||||
    state_ready = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    # When the state last actually changed, or the date of instance creation
 | 
			
		||||
    state_changed = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,9 @@
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <h3 class="author">
 | 
			
		||||
        {{ post.author.name_or_handle }} <small>@{{ post.author.handle }}</small>
 | 
			
		||||
        <a href="{{ post.author.urls.view }}">
 | 
			
		||||
            {{ post.author.name_or_handle }} <small>@{{ post.author.handle }}</small>
 | 
			
		||||
        </a>
 | 
			
		||||
    </h3>
 | 
			
		||||
    <time>
 | 
			
		||||
        <a href="{{ post.urls.view }}">{{ post.created | timesince }} ago</a>
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=False)),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=True)),
 | 
			
		||||
                ("state_changed", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("state_attempted", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("state_locked_until", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
@ -158,7 +158,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=False)),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=True)),
 | 
			
		||||
                ("state_changed", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("state_attempted", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("state_locked_until", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
@ -230,7 +230,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
                (
 | 
			
		||||
                    "users",
 | 
			
		||||
                    models.ManyToManyField(
 | 
			
		||||
                        related_name="identities", to=settings.AUTH_USER_MODEL
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        related_name="identities",
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
@ -286,7 +288,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=False)),
 | 
			
		||||
                ("state_ready", models.BooleanField(default=True)),
 | 
			
		||||
                ("state_changed", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("state_attempted", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("state_locked_until", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
 | 
			
		||||
@ -36,12 +36,12 @@ class ViewIdentity(TemplateView):
 | 
			
		||||
            local=False,
 | 
			
		||||
            fetch=True,
 | 
			
		||||
        )
 | 
			
		||||
        statuses = identity.statuses.all()[:100]
 | 
			
		||||
        posts = identity.posts.all()[:100]
 | 
			
		||||
        if identity.data_age > settings.IDENTITY_MAX_AGE:
 | 
			
		||||
            identity.transition_perform(IdentityStates.outdated)
 | 
			
		||||
        return {
 | 
			
		||||
            "identity": identity,
 | 
			
		||||
            "statuses": statuses,
 | 
			
		||||
            "posts": posts,
 | 
			
		||||
            "follow": Follow.maybe_get(self.request.identity, identity)
 | 
			
		||||
            if self.request.identity
 | 
			
		||||
            else None,
 | 
			
		||||
@ -232,9 +232,11 @@ class Inbox(View):
 | 
			
		||||
        if not identity.verify_signature(
 | 
			
		||||
            signature_details["signature"], headers_string
 | 
			
		||||
        ):
 | 
			
		||||
            print("Bad signature!")
 | 
			
		||||
            print(document)
 | 
			
		||||
            return HttpResponseUnauthorized("Bad signature")
 | 
			
		||||
        # Hand off the item to be processed by the queue
 | 
			
		||||
        InboxMessage.objects.create(message=document, state_ready=True)
 | 
			
		||||
        InboxMessage.objects.create(message=document)
 | 
			
		||||
        return HttpResponse(status=202)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user