activitypub.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2020 - Copyright ...
  4. Authors:
  5. zPlus <zplus@peers.community>
  6. """
  7. import datetime
  8. import logging
  9. import pyld
  10. import random
  11. import requests
  12. import string
  13. from . import APP_URL
  14. from . import model
  15. from . import tasks
  16. from . import settings
  17. log = logging.getLogger(__name__)
  18. # List of HTTP headers used by ActivityPub.
  19. # https://www.w3.org/TR/activitypub/#server-to-server-interactions
  20. default_header = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
  21. optional_header = 'application/activity+json'
  22. headers = [ default_header, optional_header ]
  23. # Headers to use when GETting/POSTing remote objects
  24. REQUEST_HEADERS = { 'Accept': default_header,
  25. 'Content-Type': default_header}
  26. # The JSON-LD context to use with ActivityPub activities.
  27. jsonld_context = [
  28. 'https://www.w3.org/ns/activitystreams',
  29. 'https://w3id.org/security/v1',
  30. 'https://forgefed.peers.community/ns' ]
  31. # Fetch an ActivityPub object
  32. def fetch(uri):
  33. response = requests.get(uri, headers=REQUEST_HEADERS)
  34. if response.status_code != 200:
  35. log.info('[{}] Error while retrieving remote object: {}'.format(response.status_code, uri))
  36. return None
  37. # The remote server is expected to serve a JSON-LD document.
  38. object = response.json()
  39. # Because JSON-LD can represent the same graph in several different
  40. # ways, we should normalize the JSONLD object before passing it to the
  41. # actor for processing. This simplifies working with the object.
  42. # Normalization could mean "flattening" or "compaction" of the JSONLD
  43. # document.
  44. # However, this step is left out for now and not implemented unless
  45. # needed because the ActivityStream specs already specifies that
  46. # objects should be served in compact form:
  47. # https://www.w3.org/TR/social-web-protocols/#content-representation
  48. #
  49. # object = response.json()
  50. # return normalize(object)
  51. return Document(object)
  52. # Cache a copy of the JSON-LD context such that we can work with activities
  53. # without sending HTTP requests all the time.
  54. cached_jsonld_context = []
  55. for context in jsonld_context:
  56. cached_jsonld_context.append(requests.get(context, headers=REQUEST_HEADERS).json())
  57. def format_datetime(dt):
  58. """
  59. This function is used to format a datetime object into a string that is
  60. suitable for publishing in Activities.
  61. """
  62. return dt.replace(microsecond=0) \
  63. .replace(tzinfo=datetime.timezone.utc) \
  64. .isoformat()
  65. def new_activity_id(length=32):
  66. """
  67. Generate a random string suitable for using as part of an Activity ID.
  68. """
  69. symbols = string.ascii_lowercase + string.digits
  70. return ''.join([ random.choice(symbols) for i in range(length) ])
  71. class Document(dict):
  72. def __init__(self, *args, **kwargs):
  73. super().__init__(*args, **kwargs)
  74. def match(self, pattern):
  75. """
  76. Check if this Document matches another dictionary. If this Document
  77. contains all the keys in :pattern:, and they have the same value, then
  78. the function returns True. Otherwise return False.
  79. To only check for existence of a key, not its value, use the empty
  80. dictionary like this: pattern={"key": {}}
  81. :param pattern: The Document (or Python dictionary) to match this Document against.
  82. """
  83. for key, value in pattern.items():
  84. if key not in self:
  85. return False
  86. # If the pattern is a dict, then recourse
  87. if isinstance(value, dict):
  88. if not Document(self[key] if isinstance(self[key], dict)
  89. else {}
  90. ).match(value):
  91. return False
  92. else:
  93. if self[key] != value:
  94. return False
  95. return True
  96. def first(self, property):
  97. """
  98. Return value if it's a scalar, otherwise return the first element if it
  99. is an array.
  100. """
  101. if isinstance(self[property], list):
  102. return self[property][0]
  103. if isinstance(self[property], dict):
  104. return None
  105. return self[property]
  106. def node(self, property):
  107. """
  108. Return the value if it's an object. If it's a string, try to fetch a
  109. URI.
  110. """
  111. if isinstance(self[property], dict):
  112. return Document(self[property])
  113. if isinstance(self[property], str):
  114. self[property] = fetch(self[property])
  115. return self.node(property)
  116. return None
  117. class Activity(Document):
  118. def __init__(self, *args, **kwargs):
  119. super().__init__(*args, **kwargs)
  120. self.update(kwargs)
  121. self.update({
  122. '@context': jsonld_context
  123. })
  124. def distribute(self):
  125. tasks.delivery.distribute.delay(self)
  126. class Follow(Activity):
  127. def __init__(self, *args, **kwargs):
  128. super().__init__(*args, **kwargs)
  129. self.update({ 'type': 'Follow' })
  130. class Accept(Activity):
  131. def __init__(self, *args, **kwargs):
  132. super().__init__(*args, **kwargs)
  133. self.update({ 'type': 'Accept' })