8 import simplejson as json
10 from Crypto.PublicKey import RSA
11 from .database import DATABASE
12 from .http_debug import http_debug
15 # generate actor keys if not present
16 if "actorKeys" not in DATABASE:
17 logging.info("No actor keys present, generating 4096-bit RSA keypair.")
19 privkey = RSA.generate(4096)
20 pubkey = privkey.publickey()
22 DATABASE["actorKeys"] = {
23 "publicKey": pubkey.exportKey('PEM'),
24 "privateKey": privkey.exportKey('PEM')
28 PRIVKEY = RSA.importKey(DATABASE["actorKeys"]["privateKey"])
29 PUBKEY = PRIVKEY.publickey()
32 from . import app, CONFIG
33 from .remote_actor import fetch_actor
36 AP_CONFIG = CONFIG.get('ap', {'host': 'localhost','blocked_instances':[]})
39 async def actor(request):
41 "@context": "https://www.w3.org/ns/activitystreams",
43 "sharedInbox": "https://{}/inbox".format(request.host)
45 "followers": "https://{}/followers".format(request.host),
46 "following": "https://{}/following".format(request.host),
47 "inbox": "https://{}/inbox".format(request.host),
48 "sharedInbox": "https://{}/inbox".format(request.host),
49 "name": "ActivityRelay",
50 "type": "Application",
51 "id": "https://{}/actor".format(request.host),
53 "id": "https://{}/actor#main-key".format(request.host),
54 "owner": "https://{}/actor".format(request.host),
55 "publicKeyPem": DATABASE["actorKeys"]["publicKey"]
57 "summary": "ActivityRelay bot",
58 "preferredUsername": "relay",
59 "url": "https://{}/actor".format(request.host)
61 return aiohttp.web.json_response(data)
64 app.router.add_get('/actor', actor)
67 from .http_signatures import sign_headers
70 get_actor_inbox = lambda actor: actor.get('endpoints', {}).get('sharedInbox', actor['inbox'])
73 async def push_message_to_actor(actor, message, our_key_id):
74 inbox = get_actor_inbox(actor)
76 url = urllib.parse.urlsplit(inbox)
79 data = json.dumps(message)
81 '(request-target)': 'post {}'.format(url.path),
82 'Content-Length': str(len(data)),
83 'Content-Type': 'application/activity+json',
84 'User-Agent': 'ActivityRelay'
86 headers['signature'] = sign_headers(headers, PRIVKEY, our_key_id)
88 logging.debug('%r >> %r', inbox, message)
90 async with aiohttp.ClientSession(trace_configs=[http_debug()]) as session:
91 async with session.post(inbox, data=data, headers=headers) as resp:
92 resp_payload = await resp.text()
93 logging.debug('%r >> resp %r', inbox, resp_payload)
96 async def follow_remote_actor(actor_uri):
97 logging.info('following: %r', actor_uri)
99 actor = await fetch_actor(actor_uri)
102 "@context": "https://www.w3.org/ns/activitystreams",
105 "object": actor['id'],
106 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
107 "actor": "https://{}/actor".format(AP_CONFIG['host'])
109 await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
112 async def unfollow_remote_actor(actor_uri):
113 logging.info('unfollowing: %r', actor_uri)
115 actor = await fetch_actor(actor_uri)
118 "@context": "https://www.w3.org/ns/activitystreams",
124 "actor": actor['id'],
125 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4())
127 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
128 "actor": "https://{}/actor".format(AP_CONFIG['host'])
130 await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
133 tag_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
134 def strip_html(data):
135 no_tags = tag_re.sub('', data)
136 return cgi.escape(no_tags)
139 def distill_inboxes(actor):
142 inbox = get_actor_inbox(actor)
143 targets = [target for target in DATABASE.get('relay-list', []) if target != inbox]
145 assert inbox not in targets
150 def distill_object_id(activity):
151 logging.debug('>> determining object ID for %r', activity['object'])
152 obj = activity['object']
154 if isinstance(obj, str):
160 async def handle_relay(actor, data, request):
161 object_id = distill_object_id(data)
163 # don't relay mastodon announces -- causes LRP fake direction issues
164 if data['type'] == 'Announce' and len(data.get('cc', [])) > 0:
168 "@context": "https://www.w3.org/ns/activitystreams",
170 "to": ["https://{}/actor/followers".format(request.host)],
171 "actor": "https://{}/actor".format(request.host),
173 "id": "https://{}/activities/{}".format(request.host, uuid.uuid4())
176 logging.debug('>> relay: %r', message)
178 inboxes = distill_inboxes(actor)
180 futures = [push_message_to_actor({'inbox': inbox}, message, 'https://{}/actor#main-key'.format(request.host)) for inbox in inboxes]
181 asyncio.ensure_future(asyncio.gather(*futures))
184 async def handle_follow(actor, data, request):
187 following = DATABASE.get('relay-list', [])
188 inbox = get_actor_inbox(actor)
190 if urllib.parse.urlsplit(inbox).hostname in AP_CONFIG['blocked_instances']:
193 if inbox not in following:
195 DATABASE['relay-list'] = following
198 "@context": "https://www.w3.org/ns/activitystreams",
201 "actor": "https://{}/actor".format(request.host),
203 # this is wrong per litepub, but mastodon < 2.4 is not compliant with that profile.
207 "object": "https://{}/actor".format(request.host),
211 "id": "https://{}/activities/{}".format(request.host, uuid.uuid4()),
214 asyncio.ensure_future(push_message_to_actor(actor, message, 'https://{}/actor#main-key'.format(request.host)))
216 if data['object'].endswith('/actor'):
217 asyncio.ensure_future(follow_remote_actor(actor['id']))
220 async def handle_undo(actor, data, request):
223 child = data['object']
224 if child['type'] == 'Follow':
225 following = DATABASE.get('relay-list', [])
227 inbox = get_actor_inbox(actor)
229 if inbox in following:
230 following.remove(inbox)
231 DATABASE['relay-list'] = following
233 if child['object'].endswith('/actor'):
234 await unfollow_remote_actor(actor['id'])
238 'Announce': handle_relay,
239 'Create': handle_relay,
240 'Follow': handle_follow,
245 async def inbox(request):
246 data = await request.json()
248 if 'actor' not in data or not request['validated']:
249 raise aiohttp.web.HTTPUnauthorized(body='access denied', content_type='text/plain')
251 actor = await fetch_actor(data["actor"])
252 actor_uri = 'https://{}/actor'.format(request.host)
254 logging.debug(">> payload %r", data)
256 processor = processors.get(data['type'], None)
258 await processor(actor, data, request)
260 return aiohttp.web.Response(body=b'{}', content_type='application/activity+json')
263 app.router.add_post('/inbox', inbox)