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