diff --git a/api/schemas.py b/api/schemas.py
index 0f0441c9c..1bcffeca8 100644
--- a/api/schemas.py
+++ b/api/schemas.py
@@ -71,6 +71,7 @@ class Account(Schema):
bot: bool
group: bool
discoverable: bool
+ indexable: bool
moved: Union[None, bool, "Account"]
suspended: bool = False
limited: bool = False
diff --git a/core/ld.py b/core/ld.py
index 9a5f7f913..0b7513cc2 100644
--- a/core/ld.py
+++ b/core/ld.py
@@ -557,6 +557,7 @@
"@context": {
"toot": "http://joinmastodon.org/ns#",
"discoverable": "toot:discoverable",
+ "indexable": "toot:indexable",
"devices": "toot:devices",
"featured": "toot:featured",
"featuredTags": "toot:featuredTags",
diff --git a/templates/identity/create.html b/templates/identity/create.html
index 8d353d206..770a055f4 100644
--- a/templates/identity/create.html
+++ b/templates/identity/create.html
@@ -31,6 +31,7 @@
Create
diff --git a/templates/settings/profile.html b/templates/settings/profile.html
index 1ceabea38..1260de78d 100644
--- a/templates/settings/profile.html
+++ b/templates/settings/profile.html
@@ -24,6 +24,7 @@
Privacy
{% include "forms/_field.html" with field=form.discoverable %}
+ {% include "forms/_field.html" with field=form.indexable %}
{% include "forms/_field.html" with field=form.visible_follows %}
{% include "forms/_field.html" with field=form.search_enabled %}
diff --git a/tests/users/models/test_identity.py b/tests/users/models/test_identity.py
index 3b8ac356f..5c7a9a853 100644
--- a/tests/users/models/test_identity.py
+++ b/tests/users/models/test_identity.py
@@ -204,6 +204,8 @@ def test_fetch_actor(httpx_mock, config_system):
assert identity.image_uri == "https://example.com/image.jpg"
assert identity.summary == "
A test user
"
assert "ts-a-faaaake" in identity.public_key
+ # convention is that indexability should be opt-in
+ assert not identity.indexable
@pytest.mark.django_db
diff --git a/users/migrations/0023_identity_indexable.py b/users/migrations/0023_identity_indexable.py
new file mode 100644
index 000000000..bd2d420b1
--- /dev/null
+++ b/users/migrations/0023_identity_indexable.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.3 on 2023-11-16 13:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("users", "0022_follow_request"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="identity",
+ name="indexable",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/users/models/identity.py b/users/models/identity.py
index 8912b33a9..0c98db83b 100644
--- a/users/models/identity.py
+++ b/users/models/identity.py
@@ -195,6 +195,7 @@ class Restriction(models.IntegerChoices):
summary = models.TextField(blank=True, null=True)
manually_approves_followers = models.BooleanField(blank=True, null=True)
discoverable = models.BooleanField(default=True)
+ indexable = models.BooleanField(default=False)
profile_uri = models.CharField(max_length=500, blank=True, null=True)
inbox_uri = models.CharField(max_length=500, blank=True, null=True)
@@ -557,6 +558,7 @@ def to_ap(self):
"published": self.created.strftime("%Y-%m-%dT%H:%M:%SZ"),
"url": self.absolute_profile_uri(),
"toot:discoverable": self.discoverable,
+ "toot:indexable": self.indexable,
}
if self.name:
response["name"] = self.name
@@ -914,6 +916,7 @@ def fetch_actor(self) -> bool:
self.icon_uri = get_first_image_url(document.get("icon", None))
self.image_uri = get_first_image_url(document.get("image", None))
self.discoverable = document.get("toot:discoverable", True)
+ self.indexable = document.get("toot:indexable", False)
# Profile links/metadata
self.metadata = []
for attachment in get_list(document, "attachment"):
@@ -1051,6 +1054,7 @@ def to_mastodon_json(self, source=False, include_counts=True):
"bot": self.actor_type.lower() in ["service", "application"],
"group": self.actor_type.lower() == "group",
"discoverable": self.discoverable,
+ "indexable": self.indexable,
"suspended": False,
"limited": False,
"created_at": format_ld_date(
diff --git a/users/services/identity.py b/users/services/identity.py
index 20b9984d3..89aec64d6 100644
--- a/users/services/identity.py
+++ b/users/services/identity.py
@@ -36,6 +36,7 @@ def create(
domain: Domain,
name: str,
discoverable: bool = True,
+ indexable: bool = False,
) -> Identity:
identity = Identity.objects.create(
actor_uri=f"https://{domain.uri_domain}/@{username}@{domain.domain}/",
@@ -44,6 +45,7 @@ def create(
name=name,
local=True,
discoverable=discoverable,
+ indexable=indexable,
)
identity.users.add(user)
identity.generate_keypair()
diff --git a/users/views/identity.py b/users/views/identity.py
index 028e49a95..737239791 100644
--- a/users/views/identity.py
+++ b/users/views/identity.py
@@ -313,6 +313,14 @@ class form_class(forms.Form):
),
required=False,
)
+ indexable = forms.BooleanField(
+ help_text="Should this user's activities be indexable on other servers.",
+ initial=False,
+ widget=forms.Select(
+ choices=[(True, "Indexable"), (False, "Not Indexable")]
+ ),
+ required=False,
+ )
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -385,6 +393,7 @@ def form_valid(self, form):
domain=domain_instance,
name=form.cleaned_data["name"],
discoverable=form.cleaned_data["discoverable"],
+ indexable=form.cleaned_data["indexable"],
)
self.request.session["identity_id"] = identity.id
return redirect(identity.urls.view)
diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py
index 2573d3cb8..2a5a16d39 100644
--- a/users/views/settings/profile.py
+++ b/users/views/settings/profile.py
@@ -43,6 +43,13 @@ class form_class(forms.Form):
),
required=False,
)
+ indexable = forms.BooleanField(
+ help_text="Opt-in to be indexed for search on other servers.\n(Disabling this does not guarantee third-party servers won't index your posts without permission)",
+ widget=forms.Select(
+ choices=[(True, "Indexable"), (False, "Not Indexable")]
+ ),
+ required=False,
+ )
visible_follows = forms.BooleanField(
help_text="Whether or not to show your following and follower counts in your profile",
widget=forms.Select(choices=[(True, "Visible"), (False, "Hidden")]),
@@ -93,6 +100,7 @@ def get_initial(self):
"icon": self.identity.icon and self.identity.icon.url,
"image": self.identity.image and self.identity.image.url,
"discoverable": self.identity.discoverable,
+ "indexable": self.identity.indexable,
"visible_follows": self.identity.config_identity.visible_follows,
"metadata": self.identity.metadata or [],
"search_enabled": self.identity.config_identity.search_enabled,
@@ -104,6 +112,7 @@ def form_valid(self, form):
service = IdentityService(self.identity)
self.identity.name = form.cleaned_data["name"]
self.identity.discoverable = form.cleaned_data["discoverable"]
+ self.identity.indexable = form.cleaned_data["indexable"]
service.set_summary(form.cleaned_data["summary"])
# Resize images
icon = form.cleaned_data.get("icon")