Migration reset, start of docs, env vars
This commit is contained in:
parent
1b44a25331
commit
81de10b70c
|
@ -2,5 +2,6 @@
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.venv
|
.venv
|
||||||
/*.env
|
/*.env
|
||||||
|
/docs/_build
|
||||||
/media/
|
/media/
|
||||||
notes.md
|
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.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import activities.models.fan_out
|
||||||
import activities.models.post
|
import activities.models.post
|
||||||
|
import activities.models.post_attachment
|
||||||
|
import activities.models.post_interaction
|
||||||
|
import core.uploads
|
||||||
import stator.models
|
import stator.models
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +49,12 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("local", models.BooleanField()),
|
("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",
|
"visibility",
|
||||||
models.IntegerField(
|
models.IntegerField(
|
||||||
|
@ -63,26 +75,222 @@ class Migration(migrations.Migration):
|
||||||
"in_reply_to",
|
"in_reply_to",
|
||||||
models.CharField(blank=True, max_length=500, null=True),
|
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)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated", models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
(
|
(
|
||||||
"author",
|
"author",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
related_name="statuses",
|
related_name="posts",
|
||||||
to="users.identity",
|
to="users.identity",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"mentions",
|
"mentions",
|
||||||
models.ManyToManyField(
|
models.ManyToManyField(
|
||||||
related_name="posts_mentioning", to="users.identity"
|
blank=True, related_name="posts_mentioning", to="users.identity"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"to",
|
"to",
|
||||||
models.ManyToManyField(
|
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(
|
models.CharField(
|
||||||
choices=[
|
choices=[
|
||||||
("post", "Post"),
|
("post", "Post"),
|
||||||
("mention", "Mention"),
|
|
||||||
("like", "Like"),
|
|
||||||
("follow", "Follow"),
|
|
||||||
("boost", "Boost"),
|
("boost", "Boost"),
|
||||||
|
("mentioned", "Mentioned"),
|
||||||
|
("liked", "Liked"),
|
||||||
|
("followed", "Followed"),
|
||||||
|
("boosted", "Boosted"),
|
||||||
],
|
],
|
||||||
max_length=100,
|
max_length=100,
|
||||||
),
|
),
|
||||||
|
@ -140,15 +349,25 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="timeline_events_about_us",
|
related_name="timeline_events",
|
||||||
to="activities.post",
|
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={
|
options={
|
||||||
"index_together": {
|
"index_together": {
|
||||||
("identity", "type", "subject_post", "subject_identity"),
|
|
||||||
("identity", "type", "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"
|
name = "core"
|
||||||
|
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
from core.models import Config
|
|
||||||
|
|
||||||
Config.system = Config.load_system()
|
|
||||||
jsonld.set_document_loader(builtin_document_loader)
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import core.uploads
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("users", "0002_identity_public_key_id"),
|
("users", "0001_initial"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -32,7 +36,11 @@ class Migration(migrations.Migration):
|
||||||
(
|
(
|
||||||
"image",
|
"image",
|
||||||
models.ImageField(
|
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_icon: UploadedImage = static("img/icon-128.png")
|
||||||
site_banner: UploadedImage = static("img/fjords-banner-600.jpg")
|
site_banner: UploadedImage = static("img/fjords-banner-600.jpg")
|
||||||
|
|
||||||
signup_allowed: bool = False
|
signup_allowed: bool = True
|
||||||
signup_invite_only: bool = False
|
signup_invite_only: bool = False
|
||||||
signup_text: str = ""
|
signup_text: str = ""
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
bleach~=5.0.1
|
||||||
pydantic~=1.10.2
|
pydantic~=1.10.2
|
||||||
django-htmx~=1.13.0
|
django-htmx~=1.13.0
|
||||||
|
django-storages[google,boto3]~=1.13.1
|
||||||
|
|
|
@ -549,6 +549,10 @@ form .buttons {
|
||||||
margin: -20px 0 15px 0;
|
margin: -20px 0 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form p+.buttons {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.right-column form .buttons {
|
.right-column form .buttons {
|
||||||
margin: 5px 10px 5px 0;
|
margin: 5px 10px 5px 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from core.models import Config
|
||||||
from stator.models import StatorModel
|
from stator.models import StatorModel
|
||||||
from stator.runner import StatorRunner
|
from stator.runner import StatorRunner
|
||||||
|
|
||||||
|
@ -22,6 +23,8 @@ class Command(BaseCommand):
|
||||||
parser.add_argument("model_labels", nargs="*", type=str)
|
parser.add_argument("model_labels", nargs="*", type=str)
|
||||||
|
|
||||||
def handle(self, model_labels: List[str], concurrency: int, *args, **options):
|
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
|
# Resolve the models list into names
|
||||||
models = cast(
|
models = cast(
|
||||||
List[Type[StatorModel]],
|
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
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
@ -56,11 +58,11 @@ WSGI_APPLICATION = "takahe.wsgi.application"
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||||
"HOST": os.environ.get("POSTGRES_HOST", "localhost"),
|
"HOST": os.environ.get("PGHOST", "localhost"),
|
||||||
"PORT": os.environ.get("POSTGRES_PORT", 5432),
|
"PORT": os.environ.get("PGPORT", 5432),
|
||||||
"NAME": os.environ.get("POSTGRES_DB", "takahe"),
|
"NAME": os.environ.get("PGDATABASE", "takahe"),
|
||||||
"USER": os.environ.get("POSTGRES_USER", "postgres"),
|
"USER": os.environ.get("PGUSER", "postgres"),
|
||||||
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
|
"PASSWORD": os.environ.get("PGPASSWORD"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,12 +111,47 @@ STATICFILES_DIRS = [
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
### User-configurable options, pulled from the environment ###
|
||||||
|
|
||||||
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
|
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
|
||||||
if "/" in MAIN_DOMAIN:
|
if "/" in MAIN_DOMAIN:
|
||||||
print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path")
|
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
|
if os.environ.get("TAKAHE_EMAIL_CONSOLE_ONLY"):
|
||||||
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")
|
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
|
# Identity views
|
||||||
path("@<handle>/", identity.ViewIdentity.as_view()),
|
path("@<handle>/", identity.ViewIdentity.as_view()),
|
||||||
path("@<handle>/actor/", activitypub.Actor.as_view()),
|
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
|
||||||
path("@<handle>/actor/inbox/", activitypub.Inbox.as_view()),
|
|
||||||
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
||||||
# Posts
|
# Posts
|
||||||
path("compose/", posts.Compose.as_view(), name="compose"),
|
path("compose/", posts.Compose.as_view(), name="compose"),
|
||||||
|
@ -109,6 +108,8 @@ urlpatterns = [
|
||||||
# Well-known endpoints
|
# Well-known endpoints
|
||||||
path(".well-known/webfinger", activitypub.Webfinger.as_view()),
|
path(".well-known/webfinger", activitypub.Webfinger.as_view()),
|
||||||
path(".well-known/host-meta", activitypub.HostMeta.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
|
# Task runner
|
||||||
path(".stator/runner/", stator.RequestRunner.as_view()),
|
path(".stator/runner/", stator.RequestRunner.as_view()),
|
||||||
# Django admin
|
# Django admin
|
||||||
|
|
|
@ -33,8 +33,10 @@
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Access Control</legend>
|
<legend>Access Control</legend>
|
||||||
{% include "forms/_field.html" with field=form.public %}
|
{% include "forms/_field.html" with field=form.public %}
|
||||||
|
{% include "forms/_field.html" with field=form.default %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
<a href="{% url "admin_domains" %}" class="button secondary left">Back</a>
|
||||||
<button>Create</button>
|
<button>Create</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Access Control</legend>
|
<legend>Access Control</legend>
|
||||||
{% include "forms/_field.html" with field=form.public %}
|
{% include "forms/_field.html" with field=form.public %}
|
||||||
|
{% include "forms/_field.html" with field=form.default %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
<a href="{{ domain.urls.root }}" class="button secondary left">Back</a>
|
||||||
<a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
|
<a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
|
||||||
<button>Save</button>
|
<button>Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
{% if domain.service_domain %}({{ domain.service_domain }}){% endif %}
|
{% if domain.service_domain %}({{ domain.service_domain }}){% endif %}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
|
{% if domain.default %}
|
||||||
|
<span class="pill">Default</span>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="option empty">You have no domains set up.</p>
|
<p class="option empty">You have no domains set up.</p>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
<a href="{% url "trigger_reset" %}" class="secondary button left">Forgot Password</a>
|
||||||
<button>Login</button>
|
<button>Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Password Reset{% endblock %}
|
{% block title %}Password Set{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form>
|
<form>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Password Reset</legend>
|
<legend>Password Set</legend>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Images</legend>
|
<legend>Images</legend>
|
||||||
{% include "forms/_field.html" with field=form.icon preview=request.identity.icon.url %}
|
{% include "forms/_field.html" with field=form.icon %}
|
||||||
{% include "forms/_field.html" with field=form.image preview=request.identity.image.url %}
|
{% include "forms/_field.html" with field=form.image %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a href="{{ request.identity.urls.view }}" class="button secondary left">View Profile</a>
|
<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
|
import functools
|
||||||
|
|
||||||
|
@ -6,10 +6,12 @@ import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import core.uploads
|
||||||
import stator.models
|
import stator.models
|
||||||
import users.models.follow
|
import users.models.follow
|
||||||
import users.models.identity
|
import users.models.identity
|
||||||
import users.models.inbox_message
|
import users.models.inbox_message
|
||||||
|
import users.models.password_reset
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -45,6 +47,7 @@ class Migration(migrations.Migration):
|
||||||
("deleted", models.BooleanField(default=False)),
|
("deleted", models.BooleanField(default=False)),
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated", models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
|
("last_seen", models.DateTimeField(auto_now_add=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"abstract": False,
|
"abstract": False,
|
||||||
|
@ -70,6 +73,7 @@ class Migration(migrations.Migration):
|
||||||
("local", models.BooleanField()),
|
("local", models.BooleanField()),
|
||||||
("blocked", models.BooleanField(default=False)),
|
("blocked", models.BooleanField(default=False)),
|
||||||
("public", models.BooleanField(default=False)),
|
("public", models.BooleanField(default=False)),
|
||||||
|
("default", models.BooleanField(default=False)),
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated", models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
(
|
(
|
||||||
|
@ -111,6 +115,25 @@ class Migration(migrations.Migration):
|
||||||
"abstract": False,
|
"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(
|
migrations.CreateModel(
|
||||||
name="UserEvent",
|
name="UserEvent",
|
||||||
fields=[
|
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(
|
migrations.CreateModel(
|
||||||
name="Identity",
|
name="Identity",
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -194,9 +259,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
upload_to=functools.partial(
|
upload_to=functools.partial(
|
||||||
users.models.identity.upload_namer,
|
core.uploads.upload_namer, *("profile_images",), **{}
|
||||||
*("profile_images",),
|
|
||||||
**{},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -206,14 +269,13 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
upload_to=functools.partial(
|
upload_to=functools.partial(
|
||||||
users.models.identity.upload_namer,
|
core.uploads.upload_namer, *("background_images",), **{}
|
||||||
*("background_images",),
|
|
||||||
**{},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("private_key", models.TextField(blank=True, null=True)),
|
("private_key", models.TextField(blank=True, null=True)),
|
||||||
("public_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)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated", models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
("fetched", models.DateTimeField(blank=True, null=True)),
|
("fetched", models.DateTimeField(blank=True, null=True)),
|
||||||
|
@ -224,6 +286,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="identities",
|
||||||
to="users.domain",
|
to="users.domain",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -302,7 +365,7 @@ class Migration(migrations.Migration):
|
||||||
("local_requested", "local_requested"),
|
("local_requested", "local_requested"),
|
||||||
("remote_requested", "remote_requested"),
|
("remote_requested", "remote_requested"),
|
||||||
("accepted", "accepted"),
|
("accepted", "accepted"),
|
||||||
("undone_locally", "undone_locally"),
|
("undone", "undone"),
|
||||||
("undone_remotely", "undone_remotely"),
|
("undone_remotely", "undone_remotely"),
|
||||||
],
|
],
|
||||||
default="unrequested",
|
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)
|
# should)
|
||||||
public = models.BooleanField(default=False)
|
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
|
# Domains can also be linked to one or more users for their private use
|
||||||
# This should be display domains ONLY
|
# This should be display domains ONLY
|
||||||
users = models.ManyToManyField("users.User", related_name="domains", blank=True)
|
users = models.ManyToManyField("users.User", related_name="domains", blank=True)
|
||||||
|
@ -52,7 +55,7 @@ class Domain(models.Model):
|
||||||
root = "/admin/domains/"
|
root = "/admin/domains/"
|
||||||
create = "/admin/domains/create/"
|
create = "/admin/domains/create/"
|
||||||
edit = "/admin/domains/{self.domain}/"
|
edit = "/admin/domains/{self.domain}/"
|
||||||
delete = "/admin/domains/{self.domain}/delete/"
|
delete = "{edit}delete/"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_remote_domain(cls, domain: str) -> "Domain":
|
def get_remote_domain(cls, domain: str) -> "Domain":
|
||||||
|
@ -81,7 +84,7 @@ class Domain(models.Model):
|
||||||
return cls.objects.filter(
|
return cls.objects.filter(
|
||||||
models.Q(public=True) | models.Q(users__id=user.id),
|
models.Q(public=True) | models.Q(users__id=user.id),
|
||||||
local=True,
|
local=True,
|
||||||
)
|
).order_by("-default", "domain")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.domain
|
return self.domain
|
||||||
|
|
|
@ -12,7 +12,7 @@ from stator.models import State, StateField, StateGraph, StatorModel
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetStates(StateGraph):
|
class PasswordResetStates(StateGraph):
|
||||||
new = State(try_interval=3)
|
new = State(try_interval=300)
|
||||||
sent = State()
|
sent = State()
|
||||||
|
|
||||||
new.transitions_to(sent)
|
new.transitions_to(sent)
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
from django.conf import settings
|
||||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
|
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
|
from activities.models import Post
|
||||||
from core.ld import canonicalise
|
from core.ld import canonicalise
|
||||||
|
from core.models import Config
|
||||||
from core.signatures import (
|
from core.signatures import (
|
||||||
HttpSignature,
|
HttpSignature,
|
||||||
LDSignature,
|
LDSignature,
|
||||||
VerificationError,
|
VerificationError,
|
||||||
VerificationFormatError,
|
VerificationFormatError,
|
||||||
)
|
)
|
||||||
|
from takahe import __version__
|
||||||
from users.models import Identity, InboxMessage
|
from users.models import Identity, InboxMessage
|
||||||
from users.shortcuts import by_handle_or_404
|
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):
|
class Webfinger(View):
|
||||||
"""
|
"""
|
||||||
Services webfinger requests
|
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")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
class Inbox(View):
|
class Inbox(View):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -41,6 +41,11 @@ class DomainCreate(FormView):
|
||||||
widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
|
widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
|
||||||
required=False,
|
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(
|
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,})$"
|
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"]
|
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):
|
def form_valid(self, form):
|
||||||
Domain.objects.create(
|
domain = Domain.objects.create(
|
||||||
domain=form.cleaned_data["domain"],
|
domain=form.cleaned_data["domain"],
|
||||||
service_domain=form.cleaned_data["service_domain"] or None,
|
service_domain=form.cleaned_data["service_domain"] or None,
|
||||||
public=form.cleaned_data["public"],
|
public=form.cleaned_data["public"],
|
||||||
|
default=form.cleaned_data["default"],
|
||||||
local=True,
|
local=True,
|
||||||
)
|
)
|
||||||
|
if domain.default:
|
||||||
|
Domain.objects.exclude(pk=domain.pk).update(default=False)
|
||||||
return redirect(Domain.urls.root)
|
return redirect(Domain.urls.root)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,21 +102,17 @@ class DomainEdit(FormView):
|
||||||
template_name = "admin/domain_edit.html"
|
template_name = "admin/domain_edit.html"
|
||||||
extra_context = {"section": "domains"}
|
extra_context = {"section": "domains"}
|
||||||
|
|
||||||
class form_class(forms.Form):
|
class form_class(DomainCreate.form_class):
|
||||||
domain = forms.CharField(
|
def __init__(self, *args, **kwargs):
|
||||||
help_text="The domain displayed as part of a user's identity.\nCannot be changed after the domain has been created.",
|
super().__init__(*args, **kwargs)
|
||||||
disabled=True,
|
self.fields["domain"].disabled = True
|
||||||
)
|
self.fields["service_domain"].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.",
|
def clean_domain(self):
|
||||||
disabled=True,
|
return self.cleaned_data["domain"]
|
||||||
required=False,
|
|
||||||
)
|
def clean_service_domain(self):
|
||||||
public = forms.BooleanField(
|
return self.cleaned_data["service_domain"]
|
||||||
help_text="If any user on this server can create identities here",
|
|
||||||
widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch(self, request, domain):
|
def dispatch(self, request, domain):
|
||||||
self.domain = get_object_or_404(
|
self.domain = get_object_or_404(
|
||||||
|
@ -110,14 +120,17 @@ class DomainEdit(FormView):
|
||||||
)
|
)
|
||||||
return super().dispatch(request)
|
return super().dispatch(request)
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data(*args, **kwargs)
|
||||||
context["domain"] = self.domain
|
context["domain"] = self.domain
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.domain.public = form.cleaned_data["public"]
|
self.domain.public = form.cleaned_data["public"]
|
||||||
|
self.domain.default = form.cleaned_data["default"]
|
||||||
self.domain.save()
|
self.domain.save()
|
||||||
|
if self.domain.default:
|
||||||
|
Domain.objects.exclude(pk=self.domain.pk).update(default=False)
|
||||||
return redirect(Domain.urls.root)
|
return redirect(Domain.urls.root)
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
@ -125,6 +138,7 @@ class DomainEdit(FormView):
|
||||||
"domain": self.domain.domain,
|
"domain": self.domain.domain,
|
||||||
"service_domain": self.domain.service_domain,
|
"service_domain": self.domain.service_domain,
|
||||||
"public": self.domain.public,
|
"public": self.domain.public,
|
||||||
|
"default": self.domain.default,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,4 +164,4 @@ class DomainDelete(TemplateView):
|
||||||
if self.domain.identities.exists():
|
if self.domain.identities.exists():
|
||||||
raise ValueError("Tried to delete domain with identities!")
|
raise ValueError("Tried to delete domain with identities!")
|
||||||
self.domain.delete()
|
self.domain.delete()
|
||||||
return redirect("/settings/system/domains/")
|
return redirect("admin_domains")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.password_validation import validate_password
|
from django.contrib.auth.password_validation import validate_password
|
||||||
from django.contrib.auth.views import LoginView, LogoutView
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
@ -50,6 +51,10 @@ class Signup(FormView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = User.objects.create(email=form.cleaned_data["email"])
|
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)
|
PasswordReset.create_for_user(user)
|
||||||
if "invite_code" in form.cleaned_data:
|
if "invite_code" in form.cleaned_data:
|
||||||
Invite.objects.filter(token=form.cleaned_data["invite_code"]).delete()
|
Invite.objects.filter(token=form.cleaned_data["invite_code"]).delete()
|
||||||
|
|
|
@ -2,11 +2,12 @@ import string
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.decorators import login_required
|
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.shortcuts import redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView, TemplateView, View
|
from django.views.generic import FormView, TemplateView, View
|
||||||
|
|
||||||
|
from core.ld import canonicalise
|
||||||
from core.models import Config
|
from core.models import Config
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
from users.models import Domain, Follow, FollowStates, Identity, IdentityStates
|
from users.models import Domain, Follow, FollowStates, Identity, IdentityStates
|
||||||
|
@ -14,16 +15,41 @@ from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
|
||||||
class ViewIdentity(TemplateView):
|
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"
|
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(
|
identity = by_handle_or_404(
|
||||||
self.request,
|
self.request,
|
||||||
handle,
|
handle,
|
||||||
local=False,
|
local=False,
|
||||||
fetch=True,
|
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]
|
posts = identity.posts.all()[:100]
|
||||||
if identity.data_age > Config.system.identity_max_age:
|
if identity.data_age > Config.system.identity_max_age:
|
||||||
identity.transition_perform(IdentityStates.outdated)
|
identity.transition_perform(IdentityStates.outdated)
|
||||||
|
@ -150,7 +176,7 @@ class CreateIdentity(FormView):
|
||||||
domain = form.cleaned_data["domain"]
|
domain = form.cleaned_data["domain"]
|
||||||
domain_instance = Domain.get_domain(domain)
|
domain_instance = Domain.get_domain(domain)
|
||||||
new_identity = Identity.objects.create(
|
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(),
|
username=username.lower(),
|
||||||
domain_id=domain,
|
domain_id=domain,
|
||||||
name=form.cleaned_data["name"],
|
name=form.cleaned_data["name"],
|
||||||
|
|
|
@ -147,8 +147,8 @@ class ProfilePage(FormView):
|
||||||
return {
|
return {
|
||||||
"name": self.request.identity.name,
|
"name": self.request.identity.name,
|
||||||
"summary": self.request.identity.summary,
|
"summary": self.request.identity.summary,
|
||||||
"icon": self.request.identity.icon.url,
|
"icon": self.request.identity.icon and self.request.identity.icon.url,
|
||||||
"image": self.request.identity.image.url,
|
"image": self.request.identity.image and self.request.identity.image.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|
Loading…
Reference in New Issue