import re from typing import Any import boto3 from botocore.client import Config from django.conf import settings from django.db import migrations from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.migrations.state import StateApps from django.db.models import Exists, OuterRef def fix_emoji_metadata(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None: if settings.LOCAL_UPLOADS_DIR is not None: return bucket = boto3.resource( "s3", aws_access_key_id=settings.S3_KEY, aws_secret_access_key=settings.S3_SECRET_KEY, region_name=settings.S3_REGION, endpoint_url=settings.S3_ENDPOINT_URL, config=Config( signature_version=None, s3={"addressing_style": settings.S3_ADDRESSING_STYLE}, ), ).Bucket(settings.S3_AVATAR_BUCKET) Realm = apps.get_model("zerver", "Realm") RealmEmoji = apps.get_model("zerver", "RealmEmoji") UserProfile = apps.get_model("zerver", "UserProfile") for realm in Realm.objects.filter( Exists(RealmEmoji.objects.filter(realm_id=OuterRef("id"))) ).only("id"): fallback_owner = ( UserProfile.objects.filter(realm=realm.id, is_bot=False, role=100, is_active=True) .order_by("id") .first() ) assert fallback_owner is not None emoji_by_filename: dict[str, Any] = {} for emoji in RealmEmoji.objects.filter(realm_id=realm.id).exclude(file_name=None): assert emoji.file_name is not None if emoji.author_id is None: emoji.author_id = fallback_owner.id emoji.save(update_fields=["author_id"]) if emoji.file_name not in emoji_by_filename: # Slack imports may have multiple RealmEmoji rows for # the same file_name; we pick one arbitrarily emoji_by_filename[emoji.file_name] = emoji for obj_summary in bucket.objects.filter(Prefix=f"{realm.id}/emoji/images/"): match = re.match(r"^\d+/emoji/images/(.+)$", obj_summary.key) assert match emoji_filename = match.group(1) if emoji_filename not in emoji_by_filename: continue emoji = emoji_by_filename[emoji_filename] assert emoji.author_id is not None head = bucket.meta.client.head_object(Bucket=bucket.name, Key=obj_summary.key) metadata = head["Metadata"] changed = False if metadata.get("realm_id") is None: metadata["realm_id"] = str(realm.id) changed = True if metadata.get("user_profile_id") is None: metadata["user_profile_id"] = str(emoji.author_id) changed = True if not changed: continue assert not head.get("ContentDisposition") obj_summary.copy_from( ContentType=head["ContentType"], CopySource=f"{bucket.name}/{obj_summary.key}", Metadata=metadata, MetadataDirective="REPLACE", ) class Migration(migrations.Migration): atomic = False dependencies = [ ("zerver", "0508_realmuserdefault_receives_typing_notifications_and_more"), ] operations = [ migrations.RunPython( fix_emoji_metadata, reverse_code=migrations.RunPython.noop, elidable=True ) ]