Migration reset, start of docs, env vars

pull/3/head
Andrew Godwin 2022-11-18 08:28:15 -07:00
rodzic 1b44a25331
commit 81de10b70c
43 zmienionych plików z 679 dodań i 679 usunięć

1
.gitignore vendored
Wyświetl plik

@ -2,5 +2,6 @@
*.sqlite3
.venv
/*.env
/docs/_build
/media/
notes.md

Wyświetl plik

@ -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"),
},
},
),

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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",
),
),
]

Wyświetl plik

@ -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,
),
),
]

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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",), **{}
),
),
),
]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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",), **{}
),
),
),
(

Wyświetl plik

@ -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",), **{}
),
),
),
]

Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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]],

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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",

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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",
),
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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)),
],
),
]

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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):
"""

Wyświetl plik

@ -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")

Wyświetl plik

@ -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()

Wyświetl plik

@ -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"],

Wyświetl plik

@ -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):