Source code for swh.coarnotify.server.handlers
# Copyright (C) 2025 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
"""Module dedicated to the handlers of COAR Notifications."""
from importlib.metadata import version
import json
from typing import Callable
from django.conf import settings
from swh.model.model import (
MetadataAuthority,
MetadataAuthorityType,
MetadataFetcher,
Origin,
RawExtrinsicMetadata,
)
from swh.storage import get_storage
from .models import InboundNotification, Statuses
from .utils import create_accept_cn, reject, send_cn, to_sorted_tuple, unprocessable
CNHandler = Callable[[InboundNotification], None]
[docs]
def get_handler(notification: InboundNotification) -> CNHandler | None:
"""Get a CN handler from its type.
The list of handlers by type is defined in the ``handlers`` dict.
Args:
notification: an inbound CN
Raises:
UnprocessableException: no handler available for cn
Returns:
A COAR Notification handler if one matches
"""
type_ = to_sorted_tuple(notification.payload["type"])
try:
return handlers[type_]
except KeyError:
error_message = f"Unable to process {', '.join(type_)} COAR Notifications"
unprocessable(notification, error_message)
return None
[docs]
def mention(notification: InboundNotification) -> None:
"""Handle a mention COAR Notification.
Validates the payload and send an Accept CN.
Args:
cn: an inbound CN
Raises:
RejectException: invalid payload
"""
# validate the payload and extract data or raise unprocessable
context_data = notification.payload["context"] # describe the software
object_data = notification.payload["object"] # describe the relationship
origin_url = context_data["id"]
if origin_url != object_data["as:object"]:
error_message = (
f"Context id {origin_url} does not match "
f"object as:object {object_data['as:object']}"
)
reject(notification, error_message)
return
context_type = to_sorted_tuple(context_data["type"])
if "sorg:SoftwareSourceCode" not in context_type:
error_message = "Context type does not contain sorg:SoftwareSourceCode"
reject(notification, error_message)
return
# TODO: validation
# relationship = object_data["as:relationship"] # required
# subject = object_data["as:subject"] # required
# verify cn.payload["origin"]["id"]
# save metadata
storage = get_storage(**settings.SWH_CONF["storage"])
metadata_fetcher = MetadataFetcher(
name="swh-coarnotify", version=version("swh-coarnotify")
)
# TODO try except
origin = Origin(origin_url)
swhid = origin.swhid()
metadata_authority = MetadataAuthority(
type=MetadataAuthorityType.REGISTRY, # XXX: use a dedicated type ?
url=notification.payload["origin"]["id"],
)
metadata_object = RawExtrinsicMetadata(
target=swhid,
discovery_date=notification.created_at,
authority=metadata_authority,
fetcher=metadata_fetcher,
format="coarnotify-mention-v1",
metadata=json.dumps(notification.payload).encode(),
)
storage.metadata_authority_add([metadata_authority])
storage.metadata_fetcher_add([metadata_fetcher])
storage.raw_extrinsic_metadata_add([metadata_object])
# update cn and send a reply
notification.status = Statuses.ACCEPTED
notification.save()
# XXX should we had an extra context here to return the swhid ?
accepted_cn = create_accept_cn(notification, summary=f"Stored mention for {swhid}")
send_cn(accepted_cn)
handlers = {
("Announce", "RelationshipAction"): mention,
}