Implement inbound account migration
This commit is contained in:
parent
cc6355f60b
commit
4a8bdec90c
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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