Implement inbound account migration
This commit is contained in:
parent
cc6355f60b
commit
4a8bdec90c
|
@ -432,6 +432,17 @@ section h1.above {
|
|||
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 {
|
||||
margin: 5px 0 10px 0;
|
||||
}
|
||||
|
@ -983,6 +994,7 @@ button,
|
|||
background-color: var(--color-highlight);
|
||||
color: var(--color-text-in-highlight);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button.delete,
|
||||
|
|
|
@ -65,6 +65,11 @@ urlpatterns = [
|
|||
settings.CsvFollowers.as_view(),
|
||||
name="settings_export_followers_csv",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/migrate_in/",
|
||||
settings.MigrateInPage.as_view(),
|
||||
name="settings_migrate_in",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/tokens/",
|
||||
settings.TokensRoot.as_view(),
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</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">
|
||||
<i class="fa-solid fa-window-restore"></i>
|
||||
<span>Authorized Apps</span>
|
||||
|
|
|
@ -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 %}
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
import ssl
|
||||
from functools import cached_property, partial
|
||||
from typing import Literal
|
||||
from typing import Literal, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
@ -201,6 +201,10 @@ class Identity(StatorModel):
|
|||
# Should be a list of object URIs (we don't want a full M2M here)
|
||||
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
|
||||
sensitive = models.BooleanField(default=False)
|
||||
restriction = models.IntegerField(
|
||||
|
@ -330,8 +334,21 @@ class Identity(StatorModel):
|
|||
self.following_uri = self.actor_uri + "following/"
|
||||
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 ###
|
||||
|
||||
@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
|
||||
def by_username_and_domain(
|
||||
cls,
|
||||
|
@ -339,7 +356,7 @@ class Identity(StatorModel):
|
|||
domain: str | Domain,
|
||||
fetch: bool = False,
|
||||
local: bool = False,
|
||||
):
|
||||
) -> Optional["Identity"]:
|
||||
"""
|
||||
Get an Identity by username and domain.
|
||||
|
||||
|
@ -543,6 +560,8 @@ class Identity(StatorModel):
|
|||
}
|
||||
for item in self.metadata
|
||||
]
|
||||
if self.aliases:
|
||||
response["alsoKnownAs"] = self.aliases
|
||||
# Emoji
|
||||
emojis = Emoji.emojis_from_content(
|
||||
(self.name or "") + " " + (self.summary or ""), None
|
||||
|
|
|
@ -11,6 +11,7 @@ from users.views.settings.import_export import ( # noqa
|
|||
ImportExportPage,
|
||||
)
|
||||
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.profile import ProfilePage # noqa
|
||||
from users.views.settings.security import SecurityPage # noqa
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue