Migration reset, start of docs, env vars
This commit is contained in:
		
							parent
							
								
									1b44a25331
								
							
						
					
					
						commit
						81de10b70c
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -2,5 +2,6 @@
 | 
			
		||||
*.sqlite3
 | 
			
		||||
.venv
 | 
			
		||||
/*.env
 | 
			
		||||
/docs/_build
 | 
			
		||||
/media/
 | 
			
		||||
notes.md
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,16 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-11 20:02
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 17:49
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import activities.models.fan_out
 | 
			
		||||
import activities.models.post
 | 
			
		||||
import activities.models.post_attachment
 | 
			
		||||
import activities.models.post_interaction
 | 
			
		||||
import core.uploads
 | 
			
		||||
import stator.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -42,7 +49,12 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("local", models.BooleanField()),
 | 
			
		||||
                ("object_uri", models.CharField(blank=True, max_length=500, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "object_uri",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        blank=True, max_length=500, null=True, unique=True
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "visibility",
 | 
			
		||||
                    models.IntegerField(
 | 
			
		||||
@ -63,26 +75,222 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "in_reply_to",
 | 
			
		||||
                    models.CharField(blank=True, max_length=500, null=True),
 | 
			
		||||
                ),
 | 
			
		||||
                ("hashtags", models.JSONField(blank=True, null=True)),
 | 
			
		||||
                ("published", models.DateTimeField(default=django.utils.timezone.now)),
 | 
			
		||||
                ("edited", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "author",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.PROTECT,
 | 
			
		||||
                        related_name="statuses",
 | 
			
		||||
                        related_name="posts",
 | 
			
		||||
                        to="users.identity",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "mentions",
 | 
			
		||||
                    models.ManyToManyField(
 | 
			
		||||
                        related_name="posts_mentioning", to="users.identity"
 | 
			
		||||
                        blank=True, related_name="posts_mentioning", to="users.identity"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "to",
 | 
			
		||||
                    models.ManyToManyField(
 | 
			
		||||
                        related_name="posts_to", to="users.identity"
 | 
			
		||||
                        blank=True, related_name="posts_to", to="users.identity"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PostInteraction",
 | 
			
		||||
            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"),
 | 
			
		||||
                            ("fanned_out", "fanned_out"),
 | 
			
		||||
                            ("undone", "undone"),
 | 
			
		||||
                            ("undone_fanned_out", "undone_fanned_out"),
 | 
			
		||||
                        ],
 | 
			
		||||
                        default="new",
 | 
			
		||||
                        graph=activities.models.post_interaction.PostInteractionStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "object_uri",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        blank=True, max_length=500, null=True, unique=True
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "type",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        choices=[("like", "Like"), ("boost", "Boost")], max_length=100
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("published", models.DateTimeField(default=django.utils.timezone.now)),
 | 
			
		||||
                ("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="interactions",
 | 
			
		||||
                        to="users.identity",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "post",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="interactions",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "index_together": {("type", "identity", "post")},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PostAttachment",
 | 
			
		||||
            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"), ("fetched", "fetched")],
 | 
			
		||||
                        default="new",
 | 
			
		||||
                        graph=activities.models.post_attachment.PostAttachmentStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("mimetype", models.CharField(max_length=200)),
 | 
			
		||||
                (
 | 
			
		||||
                    "file",
 | 
			
		||||
                    models.FileField(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        upload_to=functools.partial(
 | 
			
		||||
                            core.uploads.upload_namer, *("attachments",), **{}
 | 
			
		||||
                        ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("remote_url", models.CharField(blank=True, max_length=500, null=True)),
 | 
			
		||||
                ("name", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("width", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("height", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("focal_x", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("focal_y", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("blurhash", models.TextField(blank=True, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "post",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="attachments",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        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"),
 | 
			
		||||
                            ("interaction", "Interaction"),
 | 
			
		||||
                            ("undo_interaction", "Undo Interaction"),
 | 
			
		||||
                        ],
 | 
			
		||||
                        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",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "subject_post_interaction",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="fan_outs",
 | 
			
		||||
                        to="activities.postinteraction",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
@ -107,10 +315,11 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        choices=[
 | 
			
		||||
                            ("post", "Post"),
 | 
			
		||||
                            ("mention", "Mention"),
 | 
			
		||||
                            ("like", "Like"),
 | 
			
		||||
                            ("follow", "Follow"),
 | 
			
		||||
                            ("boost", "Boost"),
 | 
			
		||||
                            ("mentioned", "Mentioned"),
 | 
			
		||||
                            ("liked", "Liked"),
 | 
			
		||||
                            ("followed", "Followed"),
 | 
			
		||||
                            ("boosted", "Boosted"),
 | 
			
		||||
                        ],
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
@ -140,15 +349,25 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="timeline_events_about_us",
 | 
			
		||||
                        related_name="timeline_events",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "subject_post_interaction",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="timeline_events",
 | 
			
		||||
                        to="activities.postinteraction",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "index_together": {
 | 
			
		||||
                    ("identity", "type", "subject_post", "subject_identity"),
 | 
			
		||||
                    ("identity", "type", "subject_identity"),
 | 
			
		||||
                    ("identity", "type", "subject_post", "subject_identity"),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -1,103 +0,0 @@
 | 
			
		||||
# 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,18 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-13 03:09
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("activities", "0002_fan_out"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="object_uri",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=500, null=True, unique=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,126 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-14 00:41
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import activities.models.post_interaction
 | 
			
		||||
import stator.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0002_identity_public_key_id"),
 | 
			
		||||
        ("activities", "0003_alter_post_object_uri"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            old_name="authored",
 | 
			
		||||
            new_name="published",
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="fanout",
 | 
			
		||||
            name="type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[("post", "Post"), ("interaction", "Interaction")],
 | 
			
		||||
                max_length=100,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="timelineevent",
 | 
			
		||||
            name="subject_post",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                related_name="timeline_events",
 | 
			
		||||
                to="activities.post",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PostInteraction",
 | 
			
		||||
            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"), ("fanned_out", "fanned_out")],
 | 
			
		||||
                        default="new",
 | 
			
		||||
                        graph=activities.models.post_interaction.PostInteractionStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "object_uri",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        blank=True, max_length=500, null=True, unique=True
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "type",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        choices=[("like", "Like"), ("boost", "Boost")], max_length=100
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("published", models.DateTimeField(default=django.utils.timezone.now)),
 | 
			
		||||
                ("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="interactions",
 | 
			
		||||
                        to="users.identity",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "post",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="interactions",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "index_together": {("type", "identity", "post")},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="fanout",
 | 
			
		||||
            name="subject_post_interaction",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                related_name="fan_outs",
 | 
			
		||||
                to="activities.postinteraction",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="timelineevent",
 | 
			
		||||
            name="subject_post_interaction",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                related_name="timeline_events",
 | 
			
		||||
                to="activities.postinteraction",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-16 20:18
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        (
 | 
			
		||||
            "activities",
 | 
			
		||||
            "0004_rename_authored_post_published_alter_fanout_type_and_more",
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="hashtags",
 | 
			
		||||
            field=models.JSONField(default=[]),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="fanout",
 | 
			
		||||
            name="type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("post", "Post"),
 | 
			
		||||
                    ("interaction", "Interaction"),
 | 
			
		||||
                    ("undo_interaction", "Undo Interaction"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=100,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="timelineevent",
 | 
			
		||||
            name="type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("post", "Post"),
 | 
			
		||||
                    ("boost", "Boost"),
 | 
			
		||||
                    ("mentioned", "Mentioned"),
 | 
			
		||||
                    ("liked", "Liked"),
 | 
			
		||||
                    ("followed", "Followed"),
 | 
			
		||||
                    ("boosted", "Boosted"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=100,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-17 04:18
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("activities", "0005_post_hashtags_alter_fanout_type_and_more"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="hashtags",
 | 
			
		||||
            field=models.JSONField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-17 04:50
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("activities", "0006_alter_post_hashtags"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="post",
 | 
			
		||||
            name="edited",
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,69 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-17 05:42
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import activities.models.post_attachment
 | 
			
		||||
import stator.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("activities", "0007_post_edited"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PostAttachment",
 | 
			
		||||
            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"), ("fetched", "fetched")],
 | 
			
		||||
                        default="new",
 | 
			
		||||
                        graph=activities.models.post_attachment.PostAttachmentStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("mimetype", models.CharField(max_length=200)),
 | 
			
		||||
                (
 | 
			
		||||
                    "file",
 | 
			
		||||
                    models.FileField(
 | 
			
		||||
                        blank=True, null=True, upload_to="attachments/%Y/%m/%d/"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("remote_url", models.CharField(blank=True, max_length=500, null=True)),
 | 
			
		||||
                ("name", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("width", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("height", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("focal_x", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("focal_y", models.IntegerField(blank=True, null=True)),
 | 
			
		||||
                ("blurhash", models.TextField(blank=True, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "post",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="attachments",
 | 
			
		||||
                        to="activities.post",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 01:40
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import core.uploads
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("activities", "0008_postattachment"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="postattachment",
 | 
			
		||||
            name="file",
 | 
			
		||||
            field=models.FileField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                upload_to=functools.partial(
 | 
			
		||||
                    core.uploads.upload_namer, *("attachments",), **{}
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -9,7 +9,4 @@ class CoreConfig(AppConfig):
 | 
			
		||||
    name = "core"
 | 
			
		||||
 | 
			
		||||
    def ready(self) -> None:
 | 
			
		||||
        from core.models import Config
 | 
			
		||||
 | 
			
		||||
        Config.system = Config.load_system()
 | 
			
		||||
        jsonld.set_document_loader(builtin_document_loader)
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,20 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-16 21:23
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 17:49
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import core.uploads
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0002_identity_public_key_id"),
 | 
			
		||||
        ("users", "0001_initial"),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -32,7 +36,11 @@ class Migration(migrations.Migration):
 | 
			
		||||
                (
 | 
			
		||||
                    "image",
 | 
			
		||||
                    models.ImageField(
 | 
			
		||||
                        blank=True, null=True, upload_to="config/%Y/%m/%d/"
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        upload_to=functools.partial(
 | 
			
		||||
                            core.uploads.upload_namer, *("config",), **{}
 | 
			
		||||
                        ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 01:40
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import core.uploads
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("core", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="config",
 | 
			
		||||
            name="image",
 | 
			
		||||
            field=models.ImageField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                upload_to=functools.partial(
 | 
			
		||||
                    core.uploads.upload_namer, *("config",), **{}
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -160,7 +160,7 @@ class Config(models.Model):
 | 
			
		||||
        site_icon: UploadedImage = static("img/icon-128.png")
 | 
			
		||||
        site_banner: UploadedImage = static("img/fjords-banner-600.jpg")
 | 
			
		||||
 | 
			
		||||
        signup_allowed: bool = False
 | 
			
		||||
        signup_allowed: bool = True
 | 
			
		||||
        signup_invite_only: bool = False
 | 
			
		||||
        signup_text: str = ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
# Minimal makefile for Sphinx documentation
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
# You can set these variables from the command line, and also
 | 
			
		||||
# from the environment for the first two.
 | 
			
		||||
SPHINXOPTS    ?=
 | 
			
		||||
SPHINXBUILD   ?= sphinx-build
 | 
			
		||||
SOURCEDIR     = .
 | 
			
		||||
BUILDDIR      = _build
 | 
			
		||||
 | 
			
		||||
# Put it first so that "make" without argument is like "make help".
 | 
			
		||||
help:
 | 
			
		||||
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 | 
			
		||||
 | 
			
		||||
.PHONY: help Makefile
 | 
			
		||||
 | 
			
		||||
# Catch-all target: route all unknown targets to Sphinx using the new
 | 
			
		||||
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 | 
			
		||||
%: Makefile
 | 
			
		||||
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 | 
			
		||||
							
								
								
									
										26
									
								
								docs/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								docs/conf.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
# Configuration file for the Sphinx documentation builder.
 | 
			
		||||
#
 | 
			
		||||
# For the full list of built-in configuration values, see the documentation:
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
 | 
			
		||||
 | 
			
		||||
# -- Project information -----------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 | 
			
		||||
 | 
			
		||||
project = "Takahē"
 | 
			
		||||
copyright = "2022, Andrew Godwin"
 | 
			
		||||
author = "Andrew Godwin"
 | 
			
		||||
 | 
			
		||||
# -- General configuration ---------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
			
		||||
 | 
			
		||||
extensions: list = []
 | 
			
		||||
 | 
			
		||||
templates_path = ["_templates"]
 | 
			
		||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- Options for HTML output -------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
			
		||||
 | 
			
		||||
html_theme = "alabaster"
 | 
			
		||||
html_static_path = ["_static"]
 | 
			
		||||
							
								
								
									
										13
									
								
								docs/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								docs/index.rst
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
Takahē
 | 
			
		||||
======
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Welcome to the Takahē documentation! Takahē is an ActivityPub server, designed
 | 
			
		||||
for low- to medium-size installations, and with the ability to serve multiple
 | 
			
		||||
domains at once.
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 2
 | 
			
		||||
   :caption: Contents:
 | 
			
		||||
 | 
			
		||||
   installation
 | 
			
		||||
							
								
								
									
										76
									
								
								docs/installation.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								docs/installation.rst
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
Installation
 | 
			
		||||
============
 | 
			
		||||
 | 
			
		||||
We recommend running using the Docker/OCI image; this contains all of the
 | 
			
		||||
necessary dependencies and static file handling preconfigured for you.
 | 
			
		||||
 | 
			
		||||
All configuration is done via either environment variables, or online through
 | 
			
		||||
the web interface.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Prerequisites
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
* SSL support (Takahē *requires* HTTPS)
 | 
			
		||||
* Something that can run Docker/OCI images ("serverless" platforms are fine!)
 | 
			
		||||
* A PostgreSQL 14 (or above) database
 | 
			
		||||
* One of these to store uploaded images and media:
 | 
			
		||||
  * Amazon S3
 | 
			
		||||
  * Google Cloud Storage
 | 
			
		||||
  * Writable local directory (must be accessible by all running copies!)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Environment Variables
 | 
			
		||||
---------------------
 | 
			
		||||
 | 
			
		||||
All of these variables are *required* for a working installation, and should
 | 
			
		||||
be provided from the first boot.
 | 
			
		||||
 | 
			
		||||
* ``PGHOST``, ``PGPORT``, ``PGUSER``, ``PGDATABASE``, and ``PGPASSWORD`` are the
 | 
			
		||||
  standard PostgreSQL environment variables for configuring your database.
 | 
			
		||||
 | 
			
		||||
* ``TAKAHE_MEDIA_BACKEND`` must be one of ``local``, ``s3`` or ``gcs``.
 | 
			
		||||
 | 
			
		||||
    * If it is set to ``local``, you must also provide ``TAKAHE_MEDIA_ROOT``,
 | 
			
		||||
      the path to the local media directory, and ``TAKAHE_MEDIA_URL``, a
 | 
			
		||||
      fully-qualified URL prefix that serves that directory.
 | 
			
		||||
 | 
			
		||||
    * If it is set to ``gcs``, you must also provide ``TAKAHE_MEDIA_BUCKET``,
 | 
			
		||||
      the name of the bucket to store files in.
 | 
			
		||||
 | 
			
		||||
    * If it is set to ``s3``, you must also provide ``TAKAHE_MEDIA_BUCKET``,
 | 
			
		||||
      the name of the bucket to store files in.
 | 
			
		||||
 | 
			
		||||
* ``TAKAHE_MAIN_DOMAIN`` should be the domain name (without ``https://``) that
 | 
			
		||||
  will be used for default links (such as in emails). It does *not* need to be
 | 
			
		||||
  the same as any domain you are hosting user accounts on.
 | 
			
		||||
 | 
			
		||||
* ``TAKAHE_EMAIL_HOST`` and ``TAKAHE_EMAIL_PORT`` (along with
 | 
			
		||||
  ``TAKAHE_EMAIL_USER`` and ``TAKAHE_EMAIL_PASSWORD``, if needed) should point
 | 
			
		||||
  to an SMTP server Takahe can use for sending email. Email is *required*, to
 | 
			
		||||
  allow account creation and password resets.
 | 
			
		||||
 | 
			
		||||
  * If you are using SendGrid, you can just set an API key in
 | 
			
		||||
    ``TAKAHE_EMAIL_SENDGRID_KEY`` instead.
 | 
			
		||||
 | 
			
		||||
* ``TAKAHE_EMAIL_FROM`` is the email address that emails from the system will
 | 
			
		||||
  appear to come from.
 | 
			
		||||
 | 
			
		||||
* ``TAKAHE_AUTO_ADMIN_EMAIL`` should be an email address that you would like to
 | 
			
		||||
  be automatically promoted to administrator when it signs up. You only need
 | 
			
		||||
  this for initial setup, and can unset it after that if you like.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Making An Admin Account
 | 
			
		||||
-----------------------
 | 
			
		||||
 | 
			
		||||
Once the webserver is up and working, go to the "create account" flow and
 | 
			
		||||
create a new account using the email you specified in
 | 
			
		||||
``TAKAHE_AUTO_ADMIN_EMAIL``.
 | 
			
		||||
 | 
			
		||||
Once you set your password using the link emailed to you, you will have an
 | 
			
		||||
admin account.
 | 
			
		||||
 | 
			
		||||
If your email settings have a problem and you don't get the email, don't worry;
 | 
			
		||||
fix them and then follow the "reset my password" flow on the login screen, and
 | 
			
		||||
you'll get another password reset email that you can use.
 | 
			
		||||
							
								
								
									
										35
									
								
								docs/make.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/make.bat
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
@ECHO OFF
 | 
			
		||||
 | 
			
		||||
pushd %~dp0
 | 
			
		||||
 | 
			
		||||
REM Command file for Sphinx documentation
 | 
			
		||||
 | 
			
		||||
if "%SPHINXBUILD%" == "" (
 | 
			
		||||
	set SPHINXBUILD=sphinx-build
 | 
			
		||||
)
 | 
			
		||||
set SOURCEDIR=.
 | 
			
		||||
set BUILDDIR=_build
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% >NUL 2>NUL
 | 
			
		||||
if errorlevel 9009 (
 | 
			
		||||
	echo.
 | 
			
		||||
	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
 | 
			
		||||
	echo.installed, then set the SPHINXBUILD environment variable to point
 | 
			
		||||
	echo.to the full path of the 'sphinx-build' executable. Alternatively you
 | 
			
		||||
	echo.may add the Sphinx directory to PATH.
 | 
			
		||||
	echo.
 | 
			
		||||
	echo.If you don't have Sphinx installed, grab it from
 | 
			
		||||
	echo.https://www.sphinx-doc.org/
 | 
			
		||||
	exit /b 1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if "%1" == "" goto help
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 | 
			
		||||
goto end
 | 
			
		||||
 | 
			
		||||
:help
 | 
			
		||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 | 
			
		||||
 | 
			
		||||
:end
 | 
			
		||||
popd
 | 
			
		||||
@ -11,3 +11,4 @@ psycopg2~=2.9.5
 | 
			
		||||
bleach~=5.0.1
 | 
			
		||||
pydantic~=1.10.2
 | 
			
		||||
django-htmx~=1.13.0
 | 
			
		||||
django-storages[google,boto3]~=1.13.1
 | 
			
		||||
 | 
			
		||||
@ -549,6 +549,10 @@ form .buttons {
 | 
			
		||||
    margin: -20px 0 15px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form p+.buttons {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.right-column form .buttons {
 | 
			
		||||
    margin: 5px 10px 5px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from stator.models import StatorModel
 | 
			
		||||
from stator.runner import StatorRunner
 | 
			
		||||
 | 
			
		||||
@ -22,6 +23,8 @@ class Command(BaseCommand):
 | 
			
		||||
        parser.add_argument("model_labels", nargs="*", type=str)
 | 
			
		||||
 | 
			
		||||
    def handle(self, model_labels: List[str], concurrency: int, *args, **options):
 | 
			
		||||
        # Cache system config
 | 
			
		||||
        Config.system = Config.load_system()
 | 
			
		||||
        # Resolve the models list into names
 | 
			
		||||
        models = cast(
 | 
			
		||||
            List[Type[StatorModel]],
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-10 05:56
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 17:49
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
 | 
			
		||||
 | 
			
		||||
@ -56,11 +58,11 @@ WSGI_APPLICATION = "takahe.wsgi.application"
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "ENGINE": "django.db.backends.postgresql_psycopg2",
 | 
			
		||||
        "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
 | 
			
		||||
        "PORT": os.environ.get("POSTGRES_PORT", 5432),
 | 
			
		||||
        "NAME": os.environ.get("POSTGRES_DB", "takahe"),
 | 
			
		||||
        "USER": os.environ.get("POSTGRES_USER", "postgres"),
 | 
			
		||||
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
 | 
			
		||||
        "HOST": os.environ.get("PGHOST", "localhost"),
 | 
			
		||||
        "PORT": os.environ.get("PGPORT", 5432),
 | 
			
		||||
        "NAME": os.environ.get("PGDATABASE", "takahe"),
 | 
			
		||||
        "USER": os.environ.get("PGUSER", "postgres"),
 | 
			
		||||
        "PASSWORD": os.environ.get("PGPASSWORD"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -109,12 +111,47 @@ STATICFILES_DIRS = [
 | 
			
		||||
 | 
			
		||||
ALLOWED_HOSTS = ["*"]
 | 
			
		||||
 | 
			
		||||
### User-configurable options, pulled from the environment ###
 | 
			
		||||
 | 
			
		||||
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
 | 
			
		||||
if "/" in MAIN_DOMAIN:
 | 
			
		||||
    print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path")
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"]
 | 
			
		||||
 | 
			
		||||
# Note that this MUST be a fully qualified URL in production
 | 
			
		||||
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
 | 
			
		||||
MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")
 | 
			
		||||
if os.environ.get("TAKAHE_EMAIL_CONSOLE_ONLY"):
 | 
			
		||||
    EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 | 
			
		||||
    EMAIL_FROM = "test@example.com"
 | 
			
		||||
else:
 | 
			
		||||
    EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"]
 | 
			
		||||
    if "TAKAHE_EMAIL_SENDGRID_KEY" in os.environ:
 | 
			
		||||
        EMAIL_HOST = "smtp.sendgrid.net"
 | 
			
		||||
        EMAIL_PORT = 587
 | 
			
		||||
        EMAIL_HOST_USER: Optional[str] = "apikey"
 | 
			
		||||
        EMAIL_HOST_PASSWORD: Optional[str] = os.environ["TAKAHE_EMAIL_SENDGRID_KEY"]
 | 
			
		||||
        EMAIL_USE_TLS = True
 | 
			
		||||
    else:
 | 
			
		||||
        EMAIL_HOST = os.environ["TAKAHE_EMAIL_HOST"]
 | 
			
		||||
        EMAIL_PORT = int(os.environ["TAKAHE_EMAIL_PORT"])
 | 
			
		||||
        EMAIL_HOST_USER = os.environ.get("TAKAHE_EMAIL_USER")
 | 
			
		||||
        EMAIL_HOST_PASSWORD = os.environ.get("TAKAHE_EMAIL_PASSWORD")
 | 
			
		||||
        EMAIL_USE_SSL = EMAIL_PORT == 465
 | 
			
		||||
        EMAIL_USE_TLS = EMAIL_PORT == 587
 | 
			
		||||
 | 
			
		||||
AUTO_ADMIN_EMAIL = os.environ.get("TAKAHE_AUTO_ADMIN_EMAIL")
 | 
			
		||||
 | 
			
		||||
# Set up media storage
 | 
			
		||||
MEDIA_BACKEND = os.environ.get("TAKAHE_MEDIA_BACKEND", None)
 | 
			
		||||
if MEDIA_BACKEND == "local":
 | 
			
		||||
    # Note that this MUST be a fully qualified URL in production
 | 
			
		||||
    MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
 | 
			
		||||
    MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")
 | 
			
		||||
elif MEDIA_BACKEND == "gcs":
 | 
			
		||||
    DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage"
 | 
			
		||||
    GS_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
 | 
			
		||||
elif MEDIA_BACKEND == "s3":
 | 
			
		||||
    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
 | 
			
		||||
    AWS_STORAGE_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
 | 
			
		||||
else:
 | 
			
		||||
    print("Unknown TAKAHE_MEDIA_BACKEND value")
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
@ -86,8 +86,7 @@ urlpatterns = [
 | 
			
		||||
    ),
 | 
			
		||||
    # Identity views
 | 
			
		||||
    path("@<handle>/", identity.ViewIdentity.as_view()),
 | 
			
		||||
    path("@<handle>/actor/", activitypub.Actor.as_view()),
 | 
			
		||||
    path("@<handle>/actor/inbox/", activitypub.Inbox.as_view()),
 | 
			
		||||
    path("@<handle>/inbox/", activitypub.Inbox.as_view()),
 | 
			
		||||
    path("@<handle>/action/", identity.ActionIdentity.as_view()),
 | 
			
		||||
    # Posts
 | 
			
		||||
    path("compose/", posts.Compose.as_view(), name="compose"),
 | 
			
		||||
@ -109,6 +108,8 @@ urlpatterns = [
 | 
			
		||||
    # Well-known endpoints
 | 
			
		||||
    path(".well-known/webfinger", activitypub.Webfinger.as_view()),
 | 
			
		||||
    path(".well-known/host-meta", activitypub.HostMeta.as_view()),
 | 
			
		||||
    path(".well-known/nodeinfo", activitypub.NodeInfo.as_view()),
 | 
			
		||||
    path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()),
 | 
			
		||||
    # Task runner
 | 
			
		||||
    path(".stator/runner/", stator.RequestRunner.as_view()),
 | 
			
		||||
    # Django admin
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,10 @@
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            <legend>Access Control</legend>
 | 
			
		||||
            {% include "forms/_field.html" with field=form.public %}
 | 
			
		||||
            {% include "forms/_field.html" with field=form.default %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <a href="{% url "admin_domains" %}" class="button secondary left">Back</a>
 | 
			
		||||
            <button>Create</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,10 @@
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            <legend>Access Control</legend>
 | 
			
		||||
            {% include "forms/_field.html" with field=form.public %}
 | 
			
		||||
            {% include "forms/_field.html" with field=form.default %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <a href="{{ domain.urls.root }}" class="button secondary left">Back</a>
 | 
			
		||||
            <a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
 | 
			
		||||
            <button>Save</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,9 @@
 | 
			
		||||
                        {% if domain.service_domain %}({{ domain.service_domain }}){% endif %}
 | 
			
		||||
                    </small>
 | 
			
		||||
                </span>
 | 
			
		||||
                {% if domain.default %}
 | 
			
		||||
                    <span class="pill">Default</span>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </a>
 | 
			
		||||
        {% empty %}
 | 
			
		||||
            <p class="option empty">You have no domains set up.</p>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <a href="{% url "trigger_reset" %}" class="secondary button left">Forgot Password</a>
 | 
			
		||||
            <button>Login</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,14 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Password Reset{% endblock %}
 | 
			
		||||
{% block title %}Password Set{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <form>
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            <legend>Password Reset</legend>
 | 
			
		||||
            <legend>Password Set</legend>
 | 
			
		||||
            <p>
 | 
			
		||||
                Your password for <tt>{{ email }}</tt> has been reset!
 | 
			
		||||
                Your password for <tt>{{ email }}</tt> has been set. You can
 | 
			
		||||
                now <a href="/auth/login/">login</a>.
 | 
			
		||||
            </p>
 | 
			
		||||
        </fieldset>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,8 @@
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            <legend>Images</legend>
 | 
			
		||||
            {% include "forms/_field.html" with field=form.icon preview=request.identity.icon.url %}
 | 
			
		||||
            {% include "forms/_field.html" with field=form.image preview=request.identity.image.url %}
 | 
			
		||||
            {% include "forms/_field.html" with field=form.icon %}
 | 
			
		||||
            {% include "forms/_field.html" with field=form.image %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <a href="{{ request.identity.urls.view }}" class="button secondary left">View Profile</a>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-11 20:02
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 17:49
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
@ -6,10 +6,12 @@ import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import core.uploads
 | 
			
		||||
import stator.models
 | 
			
		||||
import users.models.follow
 | 
			
		||||
import users.models.identity
 | 
			
		||||
import users.models.inbox_message
 | 
			
		||||
import users.models.password_reset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
@ -45,6 +47,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ("deleted", models.BooleanField(default=False)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                ("last_seen", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
@ -70,6 +73,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ("local", models.BooleanField()),
 | 
			
		||||
                ("blocked", models.BooleanField(default=False)),
 | 
			
		||||
                ("public", models.BooleanField(default=False)),
 | 
			
		||||
                ("default", models.BooleanField(default=False)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
@ -111,6 +115,25 @@ class Migration(migrations.Migration):
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Invite",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.BigAutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("token", models.CharField(max_length=500, unique=True)),
 | 
			
		||||
                ("email", models.EmailField(blank=True, max_length=254, null=True)),
 | 
			
		||||
                ("note", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="UserEvent",
 | 
			
		||||
            fields=[
 | 
			
		||||
@ -146,6 +169,48 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PasswordReset",
 | 
			
		||||
            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=users.models.password_reset.PasswordResetStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("token", models.CharField(max_length=500, unique=True)),
 | 
			
		||||
                ("new_account", models.BooleanField()),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "user",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="password_resets",
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Identity",
 | 
			
		||||
            fields=[
 | 
			
		||||
@ -194,9 +259,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        upload_to=functools.partial(
 | 
			
		||||
                            users.models.identity.upload_namer,
 | 
			
		||||
                            *("profile_images",),
 | 
			
		||||
                            **{},
 | 
			
		||||
                            core.uploads.upload_namer, *("profile_images",), **{}
 | 
			
		||||
                        ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@ -206,14 +269,13 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        upload_to=functools.partial(
 | 
			
		||||
                            users.models.identity.upload_namer,
 | 
			
		||||
                            *("background_images",),
 | 
			
		||||
                            **{},
 | 
			
		||||
                            core.uploads.upload_namer, *("background_images",), **{}
 | 
			
		||||
                        ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("private_key", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("public_key", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("public_key_id", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                ("fetched", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
@ -224,6 +286,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.PROTECT,
 | 
			
		||||
                        related_name="identities",
 | 
			
		||||
                        to="users.domain",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@ -302,7 +365,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                            ("local_requested", "local_requested"),
 | 
			
		||||
                            ("remote_requested", "remote_requested"),
 | 
			
		||||
                            ("accepted", "accepted"),
 | 
			
		||||
                            ("undone_locally", "undone_locally"),
 | 
			
		||||
                            ("undone", "undone"),
 | 
			
		||||
                            ("undone_remotely", "undone_remotely"),
 | 
			
		||||
                        ],
 | 
			
		||||
                        default="unrequested",
 | 
			
		||||
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-12 21:29
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="identity",
 | 
			
		||||
            name="public_key_id",
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-17 04:18
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0002_identity_public_key_id"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="last_seen",
 | 
			
		||||
            field=models.DateTimeField(
 | 
			
		||||
                auto_now_add=True, default=django.utils.timezone.now
 | 
			
		||||
            ),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="identity",
 | 
			
		||||
            name="domain",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.PROTECT,
 | 
			
		||||
                related_name="identities",
 | 
			
		||||
                to="users.domain",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,60 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 01:40
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import stator.models
 | 
			
		||||
import users.models.password_reset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0003_user_last_seen_alter_identity_domain"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="PasswordReset",
 | 
			
		||||
            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=users.models.password_reset.PasswordResetStates,
 | 
			
		||||
                        max_length=100,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("token", models.CharField(max_length=500, unique=True)),
 | 
			
		||||
                ("new_account", models.BooleanField()),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "user",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="password_resets",
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-18 06:34
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0004_passwordreset"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Invite",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.BigAutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("token", models.CharField(max_length=500, unique=True)),
 | 
			
		||||
                ("email", models.EmailField(blank=True, max_length=254, null=True)),
 | 
			
		||||
                ("note", models.TextField(blank=True, null=True)),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -41,6 +41,9 @@ class Domain(models.Model):
 | 
			
		||||
    # should)
 | 
			
		||||
    public = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # If this is the default domain (shown as the default entry for new users)
 | 
			
		||||
    default = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # Domains can also be linked to one or more users for their private use
 | 
			
		||||
    # This should be display domains ONLY
 | 
			
		||||
    users = models.ManyToManyField("users.User", related_name="domains", blank=True)
 | 
			
		||||
@ -52,7 +55,7 @@ class Domain(models.Model):
 | 
			
		||||
        root = "/admin/domains/"
 | 
			
		||||
        create = "/admin/domains/create/"
 | 
			
		||||
        edit = "/admin/domains/{self.domain}/"
 | 
			
		||||
        delete = "/admin/domains/{self.domain}/delete/"
 | 
			
		||||
        delete = "{edit}delete/"
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_remote_domain(cls, domain: str) -> "Domain":
 | 
			
		||||
@ -81,7 +84,7 @@ class Domain(models.Model):
 | 
			
		||||
        return cls.objects.filter(
 | 
			
		||||
            models.Q(public=True) | models.Q(users__id=user.id),
 | 
			
		||||
            local=True,
 | 
			
		||||
        )
 | 
			
		||||
        ).order_by("-default", "domain")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.domain
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ from stator.models import State, StateField, StateGraph, StatorModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordResetStates(StateGraph):
 | 
			
		||||
    new = State(try_interval=3)
 | 
			
		||||
    new = State(try_interval=300)
 | 
			
		||||
    sent = State()
 | 
			
		||||
 | 
			
		||||
    new.transitions_to(sent)
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,22 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from asgiref.sync import async_to_sync
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.decorators.csrf import csrf_exempt
 | 
			
		||||
from django.views.generic import View
 | 
			
		||||
 | 
			
		||||
from activities.models import Post
 | 
			
		||||
from core.ld import canonicalise
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from core.signatures import (
 | 
			
		||||
    HttpSignature,
 | 
			
		||||
    LDSignature,
 | 
			
		||||
    VerificationError,
 | 
			
		||||
    VerificationFormatError,
 | 
			
		||||
)
 | 
			
		||||
from takahe import __version__
 | 
			
		||||
from users.models import Identity, InboxMessage
 | 
			
		||||
from users.shortcuts import by_handle_or_404
 | 
			
		||||
 | 
			
		||||
@ -37,6 +41,51 @@ class HostMeta(View):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeInfo(View):
 | 
			
		||||
    """
 | 
			
		||||
    Returns the well-known nodeinfo response, pointing to the 2.0 one
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        host = request.META.get("HOST", settings.MAIN_DOMAIN)
 | 
			
		||||
        return JsonResponse(
 | 
			
		||||
            {
 | 
			
		||||
                "links": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
 | 
			
		||||
                        "href": f"https://{host}/nodeinfo/2.0/",
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeInfo2(View):
 | 
			
		||||
    """
 | 
			
		||||
    Returns the nodeinfo 2.0 response
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        # Fetch some user stats
 | 
			
		||||
        local_identities = Identity.objects.filter(local=True).count()
 | 
			
		||||
        local_posts = Post.objects.filter(local=True).count()
 | 
			
		||||
        return JsonResponse(
 | 
			
		||||
            {
 | 
			
		||||
                "version": "2.0",
 | 
			
		||||
                "software": {"name": "takahe", "version": __version__},
 | 
			
		||||
                "protocols": ["activitypub"],
 | 
			
		||||
                "services": {"outbound": [], "inbound": []},
 | 
			
		||||
                "usage": {
 | 
			
		||||
                    "users": {"total": local_identities},
 | 
			
		||||
                    "localPosts": local_posts,
 | 
			
		||||
                },
 | 
			
		||||
                "openRegistrations": Config.system.signup_allowed
 | 
			
		||||
                and not Config.system.signup_invite_only,
 | 
			
		||||
                "metadata": {},
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Webfinger(View):
 | 
			
		||||
    """
 | 
			
		||||
    Services webfinger requests
 | 
			
		||||
@ -70,16 +119,6 @@ class Webfinger(View):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Actor(View):
 | 
			
		||||
    """
 | 
			
		||||
    Returns the AP Actor object
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get(self, request, handle):
 | 
			
		||||
        identity = by_handle_or_404(self.request, handle)
 | 
			
		||||
        return JsonResponse(canonicalise(identity.to_ap(), include_security=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(csrf_exempt, name="dispatch")
 | 
			
		||||
class Inbox(View):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,11 @@ class DomainCreate(FormView):
 | 
			
		||||
            widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
 | 
			
		||||
            required=False,
 | 
			
		||||
        )
 | 
			
		||||
        default = forms.BooleanField(
 | 
			
		||||
            help_text="If this is the default option for new identities",
 | 
			
		||||
            widget=forms.Select(choices=[(True, "Yes"), (False, "No")]),
 | 
			
		||||
            required=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        domain_regex = re.compile(
 | 
			
		||||
            r"^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$"
 | 
			
		||||
@ -72,13 +77,22 @@ class DomainCreate(FormView):
 | 
			
		||||
                )
 | 
			
		||||
            return self.cleaned_data["service_domain"]
 | 
			
		||||
 | 
			
		||||
        def clean_default(self):
 | 
			
		||||
            value = self.cleaned_data["default"]
 | 
			
		||||
            if value and not self.cleaned_data.get("public"):
 | 
			
		||||
                raise forms.ValidationError("A non-public domain cannot be the default")
 | 
			
		||||
            return value
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        Domain.objects.create(
 | 
			
		||||
        domain = Domain.objects.create(
 | 
			
		||||
            domain=form.cleaned_data["domain"],
 | 
			
		||||
            service_domain=form.cleaned_data["service_domain"] or None,
 | 
			
		||||
            public=form.cleaned_data["public"],
 | 
			
		||||
            default=form.cleaned_data["default"],
 | 
			
		||||
            local=True,
 | 
			
		||||
        )
 | 
			
		||||
        if domain.default:
 | 
			
		||||
            Domain.objects.exclude(pk=domain.pk).update(default=False)
 | 
			
		||||
        return redirect(Domain.urls.root)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -88,21 +102,17 @@ class DomainEdit(FormView):
 | 
			
		||||
    template_name = "admin/domain_edit.html"
 | 
			
		||||
    extra_context = {"section": "domains"}
 | 
			
		||||
 | 
			
		||||
    class form_class(forms.Form):
 | 
			
		||||
        domain = forms.CharField(
 | 
			
		||||
            help_text="The domain displayed as part of a user's identity.\nCannot be changed after the domain has been created.",
 | 
			
		||||
            disabled=True,
 | 
			
		||||
        )
 | 
			
		||||
        service_domain = forms.CharField(
 | 
			
		||||
            help_text="Optional - a domain that serves Takahē if it is not running on the main domain.\nCannot be changed after the domain has been created.",
 | 
			
		||||
            disabled=True,
 | 
			
		||||
            required=False,
 | 
			
		||||
        )
 | 
			
		||||
        public = forms.BooleanField(
 | 
			
		||||
            help_text="If any user on this server can create identities here",
 | 
			
		||||
            widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
 | 
			
		||||
            required=False,
 | 
			
		||||
        )
 | 
			
		||||
    class form_class(DomainCreate.form_class):
 | 
			
		||||
        def __init__(self, *args, **kwargs):
 | 
			
		||||
            super().__init__(*args, **kwargs)
 | 
			
		||||
            self.fields["domain"].disabled = True
 | 
			
		||||
            self.fields["service_domain"].disabled = True
 | 
			
		||||
 | 
			
		||||
        def clean_domain(self):
 | 
			
		||||
            return self.cleaned_data["domain"]
 | 
			
		||||
 | 
			
		||||
        def clean_service_domain(self):
 | 
			
		||||
            return self.cleaned_data["service_domain"]
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, domain):
 | 
			
		||||
        self.domain = get_object_or_404(
 | 
			
		||||
@ -110,14 +120,17 @@ class DomainEdit(FormView):
 | 
			
		||||
        )
 | 
			
		||||
        return super().dispatch(request)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self):
 | 
			
		||||
        context = super().get_context_data()
 | 
			
		||||
    def get_context_data(self, *args, **kwargs):
 | 
			
		||||
        context = super().get_context_data(*args, **kwargs)
 | 
			
		||||
        context["domain"] = self.domain
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        self.domain.public = form.cleaned_data["public"]
 | 
			
		||||
        self.domain.default = form.cleaned_data["default"]
 | 
			
		||||
        self.domain.save()
 | 
			
		||||
        if self.domain.default:
 | 
			
		||||
            Domain.objects.exclude(pk=self.domain.pk).update(default=False)
 | 
			
		||||
        return redirect(Domain.urls.root)
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
@ -125,6 +138,7 @@ class DomainEdit(FormView):
 | 
			
		||||
            "domain": self.domain.domain,
 | 
			
		||||
            "service_domain": self.domain.service_domain,
 | 
			
		||||
            "public": self.domain.public,
 | 
			
		||||
            "default": self.domain.default,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -150,4 +164,4 @@ class DomainDelete(TemplateView):
 | 
			
		||||
        if self.domain.identities.exists():
 | 
			
		||||
            raise ValueError("Tried to delete domain with identities!")
 | 
			
		||||
        self.domain.delete()
 | 
			
		||||
        return redirect("/settings/system/domains/")
 | 
			
		||||
        return redirect("admin_domains")
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.auth.password_validation import validate_password
 | 
			
		||||
from django.contrib.auth.views import LoginView, LogoutView
 | 
			
		||||
from django.shortcuts import get_object_or_404, render
 | 
			
		||||
@ -50,6 +51,10 @@ class Signup(FormView):
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        user = User.objects.create(email=form.cleaned_data["email"])
 | 
			
		||||
        # Auto-promote the user to admin if that setting is set
 | 
			
		||||
        if settings.AUTO_ADMIN_EMAIL and user.email == settings.AUTO_ADMIN_EMAIL:
 | 
			
		||||
            user.admin = True
 | 
			
		||||
            user.save()
 | 
			
		||||
        PasswordReset.create_for_user(user)
 | 
			
		||||
        if "invite_code" in form.cleaned_data:
 | 
			
		||||
            Invite.objects.filter(token=form.cleaned_data["invite_code"]).delete()
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,12 @@ import string
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.http import Http404
 | 
			
		||||
from django.http import Http404, JsonResponse
 | 
			
		||||
from django.shortcuts import redirect
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.generic import FormView, TemplateView, View
 | 
			
		||||
 | 
			
		||||
from core.ld import canonicalise
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from users.decorators import identity_required
 | 
			
		||||
from users.models import Domain, Follow, FollowStates, Identity, IdentityStates
 | 
			
		||||
@ -14,16 +15,41 @@ from users.shortcuts import by_handle_or_404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ViewIdentity(TemplateView):
 | 
			
		||||
    """
 | 
			
		||||
    Shows identity profile pages, and also acts as the Actor endpoint when
 | 
			
		||||
    approached with the right Accept header.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    template_name = "identity/view.html"
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, handle):
 | 
			
		||||
    def get(self, request, handle):
 | 
			
		||||
        # Make sure we understand this handle
 | 
			
		||||
        identity = by_handle_or_404(
 | 
			
		||||
            self.request,
 | 
			
		||||
            handle,
 | 
			
		||||
            local=False,
 | 
			
		||||
            fetch=True,
 | 
			
		||||
        )
 | 
			
		||||
        # If they're coming in looking for JSON, they want the actor
 | 
			
		||||
        accept = request.META.get("HTTP_ACCEPT", "text/html").lower()
 | 
			
		||||
        if (
 | 
			
		||||
            "application/json" in accept
 | 
			
		||||
            or "application/ld" in accept
 | 
			
		||||
            or "application/activity" in accept
 | 
			
		||||
        ):
 | 
			
		||||
            # Return actor info
 | 
			
		||||
            return self.serve_actor(identity)
 | 
			
		||||
        else:
 | 
			
		||||
            # Show normal page
 | 
			
		||||
            return super().get(request, identity=identity)
 | 
			
		||||
 | 
			
		||||
    def serve_actor(self, identity):
 | 
			
		||||
        # If this not a local actor, redirect to their canonical URI
 | 
			
		||||
        if not identity.local:
 | 
			
		||||
            return redirect(identity.actor_uri)
 | 
			
		||||
        return JsonResponse(canonicalise(identity.to_ap(), include_security=True))
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, identity):
 | 
			
		||||
        posts = identity.posts.all()[:100]
 | 
			
		||||
        if identity.data_age > Config.system.identity_max_age:
 | 
			
		||||
            identity.transition_perform(IdentityStates.outdated)
 | 
			
		||||
@ -150,7 +176,7 @@ class CreateIdentity(FormView):
 | 
			
		||||
        domain = form.cleaned_data["domain"]
 | 
			
		||||
        domain_instance = Domain.get_domain(domain)
 | 
			
		||||
        new_identity = Identity.objects.create(
 | 
			
		||||
            actor_uri=f"https://{domain_instance.uri_domain}/@{username}@{domain}/actor/",
 | 
			
		||||
            actor_uri=f"https://{domain_instance.uri_domain}/@{username}@{domain}/",
 | 
			
		||||
            username=username.lower(),
 | 
			
		||||
            domain_id=domain,
 | 
			
		||||
            name=form.cleaned_data["name"],
 | 
			
		||||
 | 
			
		||||
@ -147,8 +147,8 @@ class ProfilePage(FormView):
 | 
			
		||||
        return {
 | 
			
		||||
            "name": self.request.identity.name,
 | 
			
		||||
            "summary": self.request.identity.summary,
 | 
			
		||||
            "icon": self.request.identity.icon.url,
 | 
			
		||||
            "image": self.request.identity.image.url,
 | 
			
		||||
            "icon": self.request.identity.icon and self.request.identity.icon.url,
 | 
			
		||||
            "image": self.request.identity.image and self.request.identity.image.url,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user