Commit 979074a4 authored by jaywink's avatar jaywink
Browse files

Merge branch 'more-cross-protocol-compat' into 'master'

More cross protocol compat

See merge request !569
parents 2c502a04 8407cd95
Pipeline #5607 failed with stages
in 6 minutes and 59 seconds
......@@ -16,6 +16,12 @@ Added
See docs at :ref:`matrix-protocol-support` for more information.
* Add ``merge_remote_profiles`` management command
Attempts to merge remote profiles where the profile has both a Diaspora protocol
identifier and ActivityPub protocol identifier. The profile will be made primarily
ActivityPub. All content will be migrated to the other profile and the dupe deleted.
Changed
.......
......@@ -24,11 +30,20 @@ Changed
relay endpoint, however by default it is now unset. Also change the default value of
``SOCIALHOME_RELAY_SCOPE`` from ``all`` to ``none`` (ie not subscribing to relays).
* When receiving profiles, try to match them to an existing profile using both ActivityPub
and Diaspora protocol identifiers, to not create duplicate profiles per protocol.
* When receiving profiles, if the profile was previously set from Diaspora protocol, but
it now also has an ActivityPub ID, switch it to be an ActivityPub profile.
Fixed
.....
* Fix share retraction towards ActivityPub platforms.
* Avoid unnecessary Profile object saves when finding sender profiles and nothing for the
remote profile has changed.
0.12.1 (2020-12-12)
-------------------
......
import logging
from pprint import pprint
from django.core.management.base import BaseCommand
from django.db import transaction
from socialhome.content.models import Content
from socialhome.users.models import Profile
logger = logging.getLogger("socialhome")
counts = {
"total": 0,
"merged": 0,
"skipped_no_public_key": 0,
"no_dupes": 0,
"dupes_found": 0,
"error_saving": 0,
"aborted_guid_handle_mismatch": 0,
"contents_moved": 0,
"followers_moved": 0,
"following_moved": 0,
}
lists = {
"merged": [],
"error_saving": [],
"aborted_guid_handle_mismatch": [],
}
class Command(BaseCommand):
help = "Merge remote profiles. Attempts to merge remote profiles where the profile has both " \
"a Diaspora protocol identifier and ActivityPub protocol identifier. The profile will be " \
"made primarily ActivityPub. All content will be migrated to the other profile and the dupe " \
"deleted."
def add_arguments(self, parser):
parser.add_argument(
'--fid', dest='fid', default=None,
help='Remote profile FID to look for. If not given, will look for all.',
)
parser.add_argument(
'--max', dest='max', default=100,
help='Max amount to merge, defaults to 100.',
)
def handle(self, *args, **options):
max_merges = int(options["max"])
kwargs = {
"user__isnull": True,
"protocol": "activitypub",
}
if options.get("fid"):
kwargs["fid"] = options["fid"]
for profile in Profile.objects.filter(**kwargs).iterator():
counts["total"] += 1
print(f"Checking #{profile.id}: {profile}")
if not profile.rsa_public_key:
print(f" - skipping, profile has no public key")
counts["skipped_no_public_key"] += 1
continue
dupes = Profile.objects.filter(
rsa_public_key__contains=profile.rsa_public_key.strip(),
user__isnull=True,
protocol="diaspora",
)
if not dupes:
counts["no_dupes"] += 1
continue
# If dupes found
# There should only be one dupe, since we've only 2 protocols
dupe = dupes[0]
print(f" - dupe found: {dupe}")
counts["dupes_found"] += 1
# Last check, ensure no guid / handle weirdness
if profile.guid and profile.guid != dupe.guid:
print(f" - abort! {profile.guid} in the profile which does not match {dupe.guid}")
lists["aborted_guid_handle_mismatch"].append((profile, dupe))
counts["aborted_guid_handle_mismatch"] += 1
continue
if profile.handle and profile.handle != dupe.handle:
print(f" - abort! {profile.handle} in the profile which does not match {dupe.handle}")
lists["aborted_guid_handle_mismatch"].append((profile, dupe))
counts["aborted_guid_handle_mismatch"] += 1
continue
# Do all destructive stuff in a transaction
try:
with transaction.atomic():
# - Re-assign all content
updated = Content.objects.filter(author_id=dupe.id).update(author_id=profile.id)
print(f" - reassigned {updated} content items")
counts["contents_moved"] += updated
# - Re-assign followers and following
followers = dupe.followers.all()
followers_count = dupe.followers.count()
profile.followers.add(*followers)
print(f" - reassigned {followers_count} followers")
counts["followers_moved"] += followers_count
following = dupe.following.all()
following_count = dupe.following.count()
profile.following.add(*following)
print(f" - reassigned {following_count} following")
counts["following_moved"] += following_count
# Store info
profile.guid = dupe.guid
profile.handle = dupe.handle
print(f" - setting guid {dupe.guid} and handle {dupe.handle}")
# delete the dupe
print(f" - DELETING {dupe}")
dupe.delete()
# save the profile
print(f" - SAVING {profile}")
profile.save()
counts["merged"] += 1
lists["merged"].append(profile)
except Exception as ex:
# Something failed, we've rolled back the transaction
print(f" - ERROR: dupe was not deleted, profile was not saved: {ex}")
counts["error_saving"] += 1
lists["error_saving"].append((profile, dupe))
if counts["merged"] >= max_merges:
print(f"\n\nReached MAX {max_merges}, aborting...")
break
print("\n\n-----------------------------------------------------------\n\n")
pprint(lists)
print("\n\n-----------------------------------------------------------\n\n")
pprint(counts)
# Generated by Django 2.2.12 on 2020-05-02 21:15
from django.db import migrations
from django.db.migrations import RunPython
class Migration(migrations.Migration):
dependencies = [
('users', '0040_repopulate_profile_private_inbox'),
]
operations = [
RunPython(RunPython.noop, RunPython.noop)
]
......@@ -480,6 +480,10 @@ class Profile(TimeStampedModel):
values['handle'] = safe_text(remote_profile.handle)
values['guid'] = safe_text(remote_profile.guid)
logger.debug("from_remote_profile - values %s", values)
profile, created = Profile.objects.fed_update_or_create(fid, values)
if values["guid"]:
extra_lookups = {"guid": values["guid"]}
else:
extra_lookups = {}
profile, created = Profile.objects.fed_update_or_create(fid, values, extra_lookups)
logger.info("from_remote_profile - created %s, profile %s", created, profile)
return profile
......@@ -27,7 +27,7 @@ class ProfileQuerySet(QuerySet):
).filter(**params)
def fed_update_or_create(
self, fid: str, values: Dict[str, Any], extra_lookups: Dict=None
self, fid: str, values: Dict[str, Any], extra_lookups: Dict = None
) -> Tuple['Profile', bool]:
"""
Update or create by federated ID.
......@@ -42,11 +42,20 @@ class ProfileQuerySet(QuerySet):
values.update(extra_lookups)
return self.create(**values), True
else:
changed = False
for key, value in values.items():
if key in ('fid', 'guid', 'handle'):
continue
if getattr(profile, key, None) != value:
changed = True
setattr(profile, key, value)
profile.save()
# Switch profile to ActivityPub if Diaspora and we got an ActivityPub payload,
# indicating this is a multi-protocol remote
if profile.protocol == "diaspora" and profile.fid:
profile.protocol = "activitypub"
changed = True
if changed:
profile.save()
return profile, False
def followers(self, profile):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment