Implement inbound account migration

This commit is contained in:
Andrew Godwin 2023-07-22 11:46:35 -06:00
parent cc6355f60b
commit 4a8bdec90c
8 changed files with 146 additions and 2 deletions

View file

@ -432,6 +432,17 @@ section h1.above {
margin-bottom: -20px; margin-bottom: -20px;
} }
section h2.above {
position: relative;
top: -35px;
left: -15px;
font-weight: bold;
font-size: 100%;
text-transform: uppercase;
color: var(--color-text-dull);
margin-bottom: -20px;
}
section p { section p {
margin: 5px 0 10px 0; margin: 5px 0 10px 0;
} }
@ -983,6 +994,7 @@ button,
background-color: var(--color-highlight); background-color: var(--color-highlight);
color: var(--color-text-in-highlight); color: var(--color-text-in-highlight);
display: inline-block; display: inline-block;
text-decoration: none;
} }
button.delete, button.delete,

View file

@ -65,6 +65,11 @@ urlpatterns = [
settings.CsvFollowers.as_view(), settings.CsvFollowers.as_view(),
name="settings_export_followers_csv", name="settings_export_followers_csv",
), ),
path(
"@<handle>/settings/migrate_in/",
settings.MigrateInPage.as_view(),
name="settings_migrate_in",
),
path( path(
"@<handle>/settings/tokens/", "@<handle>/settings/tokens/",
settings.TokensRoot.as_view(), settings.TokensRoot.as_view(),

View file

@ -14,6 +14,10 @@
<i class="fa-solid fa-cloud-arrow-up"></i> <i class="fa-solid fa-cloud-arrow-up"></i>
<span>Import/Export</span> <span>Import/Export</span>
</a> </a>
<a href="{% url "settings_migrate_in" handle=identity.handle %}" {% if section == "migrate_in" %}class="selected"{% endif %} title="Interface">
<i class="fa-solid fa-door-open"></i>
<span>Migrate Inbound</span>
</a>
<a href="{% url "settings_tokens" handle=identity.handle %}" {% if section == "tokens" %}class="selected"{% endif %} title="Authorized Apps"> <a href="{% url "settings_tokens" handle=identity.handle %}" {% if section == "tokens" %}class="selected"{% endif %} title="Authorized Apps">
<i class="fa-solid fa-window-restore"></i> <i class="fa-solid fa-window-restore"></i>
<span>Authorized Apps</span> <span>Authorized Apps</span>

View file

@ -0,0 +1,36 @@
{% extends "settings/base.html" %}
{% block subtitle %}Migrate Here{% endblock %}
{% block settings_content %}
<form action="." method="POST">
{% csrf_token %}
<fieldset>
<legend>Add New Alias</legend>
<p>
To move another account to this one, first add it as an alias here,
and then go to the server where it is hosted and initiate the move.
</p>
{% include "forms/_field.html" with field=form.alias %}
</fieldset>
<div class="buttons">
<button>Add</button>
</div>
</form>
<section>
<h2 class="above">Current Aliases</h2>
<table>
{% for alias in aliases %}
<tr><td>{{ alias.handle }} <a href=".?remove_alias={{ alias.actor_uri|urlencode }}" class="button danger">Remove Alias</button></td></tr>
{% empty %}
<tr><td class="empty">You have no aliases.</td></tr>
{% endfor %}
</table>
</section>
{% endblock %}

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.1 on 2023-07-22 17:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0020_alter_identity_local"),
]
operations = [
migrations.AddField(
model_name="identity",
name="aliases",
field=models.JSONField(blank=True, null=True),
),
]

View file

@ -1,6 +1,6 @@
import ssl import ssl
from functools import cached_property, partial from functools import cached_property, partial
from typing import Literal from typing import Literal, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
@ -201,6 +201,10 @@ class Identity(StatorModel):
# Should be a list of object URIs (we don't want a full M2M here) # Should be a list of object URIs (we don't want a full M2M here)
pinned = models.JSONField(blank=True, null=True) pinned = models.JSONField(blank=True, null=True)
# A list of other actor URIs - if this account was moved, should contain
# the one URI it was moved to.
aliases = models.JSONField(blank=True, null=True)
# Admin-only moderation fields # Admin-only moderation fields
sensitive = models.BooleanField(default=False) sensitive = models.BooleanField(default=False)
restriction = models.IntegerField( restriction = models.IntegerField(
@ -330,8 +334,21 @@ class Identity(StatorModel):
self.following_uri = self.actor_uri + "following/" self.following_uri = self.actor_uri + "following/"
self.shared_inbox_uri = f"https://{self.domain.uri_domain}/inbox/" self.shared_inbox_uri = f"https://{self.domain.uri_domain}/inbox/"
def add_alias(self, actor_uri: str):
self.aliases = (self.aliases or []) + [actor_uri]
self.save()
def remove_alias(self, actor_uri: str):
self.aliases = [x for x in (self.aliases or []) if x != actor_uri]
self.save()
### Alternate constructors/fetchers ### ### Alternate constructors/fetchers ###
@classmethod
def by_handle(cls, handle, fetch: bool = False) -> Optional["Identity"]:
username, domain = handle.lstrip("@").split("@", 1)
return cls.by_username_and_domain(username=username, domain=domain, fetch=fetch)
@classmethod @classmethod
def by_username_and_domain( def by_username_and_domain(
cls, cls,
@ -339,7 +356,7 @@ class Identity(StatorModel):
domain: str | Domain, domain: str | Domain,
fetch: bool = False, fetch: bool = False,
local: bool = False, local: bool = False,
): ) -> Optional["Identity"]:
""" """
Get an Identity by username and domain. Get an Identity by username and domain.
@ -543,6 +560,8 @@ class Identity(StatorModel):
} }
for item in self.metadata for item in self.metadata
] ]
if self.aliases:
response["alsoKnownAs"] = self.aliases
# Emoji # Emoji
emojis = Emoji.emojis_from_content( emojis = Emoji.emojis_from_content(
(self.name or "") + " " + (self.summary or ""), None (self.name or "") + " " + (self.summary or ""), None

View file

@ -11,6 +11,7 @@ from users.views.settings.import_export import ( # noqa
ImportExportPage, ImportExportPage,
) )
from users.views.settings.interface import InterfacePage # noqa from users.views.settings.interface import InterfacePage # noqa
from users.views.settings.migration import MigrateInPage # noqa
from users.views.settings.posting import PostingPage # noqa from users.views.settings.posting import PostingPage # noqa
from users.views.settings.profile import ProfilePage # noqa from users.views.settings.profile import ProfilePage # noqa
from users.views.settings.security import SecurityPage # noqa from users.views.settings.security import SecurityPage # noqa

View file

@ -0,0 +1,49 @@
from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.generic import FormView
from users.models import Identity
from users.views.base import IdentityViewMixin
@method_decorator(login_required, name="dispatch")
class MigrateInPage(IdentityViewMixin, FormView):
"""
Lets the identity's profile be migrated in or out.
"""
template_name = "settings/migrate_in.html"
extra_context = {"section": "migrate_in"}
class form_class(forms.Form):
alias = forms.CharField(
help_text="The @account@example.com username you want to move here"
)
def clean_alias(self):
self.alias_identity = Identity.by_handle(
self.cleaned_data["alias"], fetch=True
)
if self.alias_identity is None:
raise forms.ValidationError("Cannot find that account.")
return self.alias_identity.actor_uri
def form_valid(self, form):
self.identity.add_alias(form.cleaned_data["alias"])
messages.info(self.request, f"Alias to {form.alias_identity.handle} added")
return redirect(".")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# If they asked for an alias deletion, do it here
if "remove_alias" in self.request.GET:
self.identity.remove_alias(self.request.GET["remove_alias"])
context["aliases"] = []
if self.identity.aliases:
context["aliases"] = [
Identity.by_actor_uri(uri) for uri in self.identity.aliases
]
return context