activity_tools.objects
1import requests 2import re 3import uuid 4from urllib.parse import urlparse 5 6from cryptography.hazmat.primitives.serialization import load_pem_public_key 7 8from .misc import ImageAsset, PublicKey, Tags, Attachment 9 10class Actor: 11 """ 12 Generic actor object. Use `create(...)` to create an actor of your own, 13 and `fetch(...)` to fetch an external actor. 14 """ 15 16 username: str 17 """ The actors username""" 18 19 def __init__(self) -> None: 20 """ 21 This creates an empty actor object 22 """ 23 self.snake_pattern = re.compile(r'(?<!^)(?=[A-Z])') 24 25 def add_property_value(self, name, value) -> None: 26 """ 27 Add a PropertyValue to the list of attachments. In Mastodon this 28 is used for the links in the profile. 29 """ 30 self.attachment.add_property_value(name, value) 31 32 def add_emoji(self, name, url) -> None: 33 """ 34 Add a custom emoji to the tag list. If you for example like to 35 map `:foo:` to `/images/foo.png`, this is the function for you! 36 """ 37 self.tag.add_emoji(name, url) 38 39 def create(self, domain: str, username: str, public_key_bytes: bytes) -> None: 40 """ 41 Populate the actor object with data. This is a useful to create the 42 actor for a user. All fields can be overriden. 43 """ 44 self.domain = domain 45 self.username = username 46 self.public_key = None 47 self.public_key_pem = public_key_bytes.decode() 48 49 self.id = f"https://{self.domain}/users/{self.username}" 50 self.type = "Person" 51 52 self.inbox = f"https://{self.domain}/users/{self.username}/inbox" 53 self.outbox = f"https://{self.domain}/users/{self.username}/outbox" 54 55 self.following = f"https://{domain}/users/{username}/following" 56 self.followers = f"https://{domain}/users/{username}/followers" 57 58 self.discoverable = False 59 self.summary = "" 60 self.published = "1523-06-06T10:00:00Z" 61 62 self.name = f"{self.username.capitalize()}" 63 self.preferred_username = f"{self.username}" 64 65 self.icon_url = None 66 self.image_url = None 67 self.manually_approves_followers = None 68 self.attachment = Attachment() 69 self.tag = Tags(self.domain) 70 71 @classmethod 72 def fetch(cls, actor_url): 73 actor = Actor() 74 actor._fetch(actor_url) 75 return actor 76 77 def _fetch(self, actor_url): 78 self.actor_raw = {} 79 80 if not actor_url: 81 raise Exception("Actor URL is not set") 82 83 headers = { 84 'Content-Type': "application/activity+json", 85 'Accept': 'application/activity+json' 86 } 87 88 actor_resp = requests.get(actor_url, headers=headers) 89 if actor_resp.status_code > 299: 90 raise Exception(f"Actor {actor_url} responded with a {actor_resp.status_code}") 91 92 self.actor_raw = actor_resp.json() 93 urlid = urlparse(self.actor_raw['id']) 94 95 self.domain = urlid.netloc 96 97 keys = [ 98 "publicKey", 99 "id", 100 "type", 101 "inbox", 102 "outbox", 103 "following", 104 "followers", 105 "discoverable", 106 "summary", 107 "published", 108 "name", 109 "preferredUsername", 110 "icon", 111 "image", 112 "manuallyApprovesFollowers", 113 "attachment", 114 "tag" 115 ] 116 117 for key in keys: 118 key_snake = self.snake_pattern.sub('_', key).lower() 119 setattr(self, key_snake, self.actor_raw.get(key)) 120 121 self.public_key_pem = self.public_key['publicKeyPem'] 122 123 def get_public_key(self): 124 return load_pem_public_key(self.public_key_pem.encode()) 125 126 def run(self) -> dict: 127 required_document = { 128 "@context": [ 129 "https://www.w3.org/ns/activitystreams", 130 "https://w3id.org/security/v1", 131 ], 132 "id": self.id, 133 "type": self.type, 134 "inbox": self.inbox, 135 "discoverable": self.discoverable, 136 "summary": self.summary, 137 "published": self.published, 138 "name": self.name, 139 "preferredUsername": self.preferred_username, 140 "attachment": self.attachment.run(), 141 "tag": self.tag.run(), 142 "publicKey": PublicKey( 143 self.domain, 144 self.username, 145 self.public_key_pem 146 ).run(), 147 } 148 149 extra_values = {} 150 151 if self.icon_url: 152 extra_values["icon"] = ImageAsset(self.icon_url) 153 154 if self.icon_url: 155 extra_values["image"] = ImageAsset(self.image_url) 156 157 if self.manually_approves_followers: 158 extra_values["manuallyApprovesFollowers"] = self.manually_approves_followers 159 160 if self.followers: 161 extra_values["followers"] = self.followers 162 163 if self.following: 164 extra_values["following"] = self.following 165 166 if self.outbox: 167 extra_values["outbox"] = self.outbox 168 169 return { **required_document, **extra_values } 170 171 172class WrapActivityStreamsObject: 173 174 def __init__(self, object) -> None: 175 self.object = object 176 177 def run(self) -> dict: 178 context = { 179 "@context": "https://www.w3.org/ns/activitystreams" 180 } 181 182 return { **context, **self.object.run() } 183 184class InboxObject: 185 186 raw: dict 187 """ 188 The raw data representing this object 189 """ 190 191 def __init__(self, data) -> None: 192 self.raw = data 193 self.id = data['id'] 194 self.type = data['type'].lower() 195 self._actor = data['actor'] 196 self._object = data['object'] 197 198 @property 199 def actor(self) -> Actor: 200 return Actor.fetch(actor_url=self._actor) 201 202 @property 203 def object(self) -> Actor: 204 return Actor.fetch(actor_url=self._object) 205 206 def run(self) -> dict: 207 """ 208 Return the raw JSON data 209 """ 210 return self.raw 211 212class Follow(InboxObject): 213 214 def __init__(self, data) -> None: 215 super().__init__(data) 216 217class Undo(InboxObject): 218 219 def __init__(self, data) -> None: 220 super().__init__(data) 221 222class ActivityPubObject: 223 224 id: str 225 """ 226 The objects ID, this should be a unique identifier 227 """ 228 229 actor: Actor 230 """ 231 The actor that created the message (the sender) 232 """ 233 234 actor_url: str 235 """ 236 A url to the actor that created the message (the sender) 237 """ 238 239 object: object 240 """ 241 A generic object representing the message/data. This may also 242 point to a target actor. 243 """ 244 245 raw: dict 246 """ 247 Object in raw unaltered form represented as a dict. This may be the 248 unaltered JSON document, but not always. 249 """ 250 251class Accept(ActivityPubObject): 252 253 object: Follow 254 """ Object representing the Follow request """ 255 256 def __init__(self, object: InboxObject) -> None: 257 self.id = f"{object.object.id}/accept/{str(uuid.uuid4())}" 258 self.object = object 259 self.raw = object.run() 260 self.actor = object.object 261 262 def run(self): 263 return { 264 "@context": "https://www.w3.org/ns/activitystreams", 265 "id": self.id, 266 "type": "Accept", 267 "actor": self.object.object.id, 268 "object": self.object.run() 269 }
class
Actor:
11class Actor: 12 """ 13 Generic actor object. Use `create(...)` to create an actor of your own, 14 and `fetch(...)` to fetch an external actor. 15 """ 16 17 username: str 18 """ The actors username""" 19 20 def __init__(self) -> None: 21 """ 22 This creates an empty actor object 23 """ 24 self.snake_pattern = re.compile(r'(?<!^)(?=[A-Z])') 25 26 def add_property_value(self, name, value) -> None: 27 """ 28 Add a PropertyValue to the list of attachments. In Mastodon this 29 is used for the links in the profile. 30 """ 31 self.attachment.add_property_value(name, value) 32 33 def add_emoji(self, name, url) -> None: 34 """ 35 Add a custom emoji to the tag list. If you for example like to 36 map `:foo:` to `/images/foo.png`, this is the function for you! 37 """ 38 self.tag.add_emoji(name, url) 39 40 def create(self, domain: str, username: str, public_key_bytes: bytes) -> None: 41 """ 42 Populate the actor object with data. This is a useful to create the 43 actor for a user. All fields can be overriden. 44 """ 45 self.domain = domain 46 self.username = username 47 self.public_key = None 48 self.public_key_pem = public_key_bytes.decode() 49 50 self.id = f"https://{self.domain}/users/{self.username}" 51 self.type = "Person" 52 53 self.inbox = f"https://{self.domain}/users/{self.username}/inbox" 54 self.outbox = f"https://{self.domain}/users/{self.username}/outbox" 55 56 self.following = f"https://{domain}/users/{username}/following" 57 self.followers = f"https://{domain}/users/{username}/followers" 58 59 self.discoverable = False 60 self.summary = "" 61 self.published = "1523-06-06T10:00:00Z" 62 63 self.name = f"{self.username.capitalize()}" 64 self.preferred_username = f"{self.username}" 65 66 self.icon_url = None 67 self.image_url = None 68 self.manually_approves_followers = None 69 self.attachment = Attachment() 70 self.tag = Tags(self.domain) 71 72 @classmethod 73 def fetch(cls, actor_url): 74 actor = Actor() 75 actor._fetch(actor_url) 76 return actor 77 78 def _fetch(self, actor_url): 79 self.actor_raw = {} 80 81 if not actor_url: 82 raise Exception("Actor URL is not set") 83 84 headers = { 85 'Content-Type': "application/activity+json", 86 'Accept': 'application/activity+json' 87 } 88 89 actor_resp = requests.get(actor_url, headers=headers) 90 if actor_resp.status_code > 299: 91 raise Exception(f"Actor {actor_url} responded with a {actor_resp.status_code}") 92 93 self.actor_raw = actor_resp.json() 94 urlid = urlparse(self.actor_raw['id']) 95 96 self.domain = urlid.netloc 97 98 keys = [ 99 "publicKey", 100 "id", 101 "type", 102 "inbox", 103 "outbox", 104 "following", 105 "followers", 106 "discoverable", 107 "summary", 108 "published", 109 "name", 110 "preferredUsername", 111 "icon", 112 "image", 113 "manuallyApprovesFollowers", 114 "attachment", 115 "tag" 116 ] 117 118 for key in keys: 119 key_snake = self.snake_pattern.sub('_', key).lower() 120 setattr(self, key_snake, self.actor_raw.get(key)) 121 122 self.public_key_pem = self.public_key['publicKeyPem'] 123 124 def get_public_key(self): 125 return load_pem_public_key(self.public_key_pem.encode()) 126 127 def run(self) -> dict: 128 required_document = { 129 "@context": [ 130 "https://www.w3.org/ns/activitystreams", 131 "https://w3id.org/security/v1", 132 ], 133 "id": self.id, 134 "type": self.type, 135 "inbox": self.inbox, 136 "discoverable": self.discoverable, 137 "summary": self.summary, 138 "published": self.published, 139 "name": self.name, 140 "preferredUsername": self.preferred_username, 141 "attachment": self.attachment.run(), 142 "tag": self.tag.run(), 143 "publicKey": PublicKey( 144 self.domain, 145 self.username, 146 self.public_key_pem 147 ).run(), 148 } 149 150 extra_values = {} 151 152 if self.icon_url: 153 extra_values["icon"] = ImageAsset(self.icon_url) 154 155 if self.icon_url: 156 extra_values["image"] = ImageAsset(self.image_url) 157 158 if self.manually_approves_followers: 159 extra_values["manuallyApprovesFollowers"] = self.manually_approves_followers 160 161 if self.followers: 162 extra_values["followers"] = self.followers 163 164 if self.following: 165 extra_values["following"] = self.following 166 167 if self.outbox: 168 extra_values["outbox"] = self.outbox 169 170 return { **required_document, **extra_values }
Generic actor object. Use create(...) to create an actor of your own,
and fetch(...) to fetch an external actor.
Actor()
20 def __init__(self) -> None: 21 """ 22 This creates an empty actor object 23 """ 24 self.snake_pattern = re.compile(r'(?<!^)(?=[A-Z])')
This creates an empty actor object
def
add_property_value(self, name, value) -> None:
26 def add_property_value(self, name, value) -> None: 27 """ 28 Add a PropertyValue to the list of attachments. In Mastodon this 29 is used for the links in the profile. 30 """ 31 self.attachment.add_property_value(name, value)
Add a PropertyValue to the list of attachments. In Mastodon this is used for the links in the profile.
def
add_emoji(self, name, url) -> None:
33 def add_emoji(self, name, url) -> None: 34 """ 35 Add a custom emoji to the tag list. If you for example like to 36 map `:foo:` to `/images/foo.png`, this is the function for you! 37 """ 38 self.tag.add_emoji(name, url)
Add a custom emoji to the tag list. If you for example like to
map :foo: to /images/foo.png, this is the function for you!
def
create(self, domain: str, username: str, public_key_bytes: bytes) -> None:
40 def create(self, domain: str, username: str, public_key_bytes: bytes) -> None: 41 """ 42 Populate the actor object with data. This is a useful to create the 43 actor for a user. All fields can be overriden. 44 """ 45 self.domain = domain 46 self.username = username 47 self.public_key = None 48 self.public_key_pem = public_key_bytes.decode() 49 50 self.id = f"https://{self.domain}/users/{self.username}" 51 self.type = "Person" 52 53 self.inbox = f"https://{self.domain}/users/{self.username}/inbox" 54 self.outbox = f"https://{self.domain}/users/{self.username}/outbox" 55 56 self.following = f"https://{domain}/users/{username}/following" 57 self.followers = f"https://{domain}/users/{username}/followers" 58 59 self.discoverable = False 60 self.summary = "" 61 self.published = "1523-06-06T10:00:00Z" 62 63 self.name = f"{self.username.capitalize()}" 64 self.preferred_username = f"{self.username}" 65 66 self.icon_url = None 67 self.image_url = None 68 self.manually_approves_followers = None 69 self.attachment = Attachment() 70 self.tag = Tags(self.domain)
Populate the actor object with data. This is a useful to create the actor for a user. All fields can be overriden.
def
run(self) -> dict:
127 def run(self) -> dict: 128 required_document = { 129 "@context": [ 130 "https://www.w3.org/ns/activitystreams", 131 "https://w3id.org/security/v1", 132 ], 133 "id": self.id, 134 "type": self.type, 135 "inbox": self.inbox, 136 "discoverable": self.discoverable, 137 "summary": self.summary, 138 "published": self.published, 139 "name": self.name, 140 "preferredUsername": self.preferred_username, 141 "attachment": self.attachment.run(), 142 "tag": self.tag.run(), 143 "publicKey": PublicKey( 144 self.domain, 145 self.username, 146 self.public_key_pem 147 ).run(), 148 } 149 150 extra_values = {} 151 152 if self.icon_url: 153 extra_values["icon"] = ImageAsset(self.icon_url) 154 155 if self.icon_url: 156 extra_values["image"] = ImageAsset(self.image_url) 157 158 if self.manually_approves_followers: 159 extra_values["manuallyApprovesFollowers"] = self.manually_approves_followers 160 161 if self.followers: 162 extra_values["followers"] = self.followers 163 164 if self.following: 165 extra_values["following"] = self.following 166 167 if self.outbox: 168 extra_values["outbox"] = self.outbox 169 170 return { **required_document, **extra_values }
class
WrapActivityStreamsObject:
class
InboxObject:
185class InboxObject: 186 187 raw: dict 188 """ 189 The raw data representing this object 190 """ 191 192 def __init__(self, data) -> None: 193 self.raw = data 194 self.id = data['id'] 195 self.type = data['type'].lower() 196 self._actor = data['actor'] 197 self._object = data['object'] 198 199 @property 200 def actor(self) -> Actor: 201 return Actor.fetch(actor_url=self._actor) 202 203 @property 204 def object(self) -> Actor: 205 return Actor.fetch(actor_url=self._object) 206 207 def run(self) -> dict: 208 """ 209 Return the raw JSON data 210 """ 211 return self.raw
Inherited Members
Inherited Members
class
ActivityPubObject:
223class ActivityPubObject: 224 225 id: str 226 """ 227 The objects ID, this should be a unique identifier 228 """ 229 230 actor: Actor 231 """ 232 The actor that created the message (the sender) 233 """ 234 235 actor_url: str 236 """ 237 A url to the actor that created the message (the sender) 238 """ 239 240 object: object 241 """ 242 A generic object representing the message/data. This may also 243 point to a target actor. 244 """ 245 246 raw: dict 247 """ 248 Object in raw unaltered form represented as a dict. This may be the 249 unaltered JSON document, but not always. 250 """
252class Accept(ActivityPubObject): 253 254 object: Follow 255 """ Object representing the Follow request """ 256 257 def __init__(self, object: InboxObject) -> None: 258 self.id = f"{object.object.id}/accept/{str(uuid.uuid4())}" 259 self.object = object 260 self.raw = object.run() 261 self.actor = object.object 262 263 def run(self): 264 return { 265 "@context": "https://www.w3.org/ns/activitystreams", 266 "id": self.id, 267 "type": "Accept", 268 "actor": self.object.object.id, 269 "object": self.object.run() 270 }
Accept(object: activity_tools.objects.InboxObject)
raw
Object in raw unaltered form represented as a dict. This may be the unaltered JSON document, but not always.