7 import simplejson as json
10 from Crypto.PublicKey import RSA
11 from .database import DATABASE
14 # generate actor keys if not present
15 if "actorKeys" not in DATABASE:
16 logging.info("No actor keys present, generating 4096-bit RSA keypair.")
18 privkey = RSA.generate(4096)
19 pubkey = privkey.publickey()
21 DATABASE["actorKeys"] = {
22 "publicKey": pubkey.exportKey('PEM'),
23 "privateKey": privkey.exportKey('PEM')
27 PRIVKEY = RSA.importKey(DATABASE["actorKeys"]["privateKey"])
28 PUBKEY = PRIVKEY.publickey()
31 from . import app, CONFIG
32 from .remote_actor import fetch_actor
35 AP_CONFIG = CONFIG.get('ap', {'host': 'localhost'})
38 async def actor(request):
40 "@context": "https://www.w3.org/ns/activitystreams",
42 "sharedInbox": "https://{}/inbox".format(request.host)
44 "followers": "https://{}/followers".format(request.host),
45 "following": "https://{}/following".format(request.host),
46 "inbox": "https://{}/inbox".format(request.host),
47 "sharedInbox": "https://{}/inbox".format(request.host),
48 "name": "ActivityRelay",
49 "type": "Application",
50 "id": "https://{}/actor".format(request.host),
52 "id": "https://{}/actor#main-key".format(request.host),
53 "owner": "https://{}/actor".format(request.host),
54 "publicKeyPem": DATABASE["actorKeys"]["publicKey"]
56 "summary": "ActivityRelay bot",
57 "preferredUsername": "relay",
58 "url": "https://{}/actor".format(request.host)
60 return aiohttp.web.json_response(data)
63 app.router.add_get('/actor', actor)
66 from .http_signatures import sign_headers
69 get_actor_inbox = lambda actor: actor.get('endpoints', {}).get('sharedInbox', actor['inbox'])
72 async def push_message_to_actor(actor, message, our_key_id):
73 inbox = get_actor_inbox(actor)
75 url = urllib.parse.urlsplit(inbox)
78 data = json.dumps(message)
80 '(request-target)': 'post {}'.format(url.path),
81 'Content-Length': str(len(data)),
82 'Content-Type': 'application/activity+json',
83 'User-Agent': 'ActivityRelay'
85 headers['signature'] = sign_headers(headers, PRIVKEY, our_key_id)
87 logging.debug('%r >> %r', inbox, message)
89 async with aiohttp.ClientSession() as session:
90 async with session.post(inbox, data=data, headers=headers) as resp:
91 resp_payload = await resp.text()
92 logging.debug('%r >> resp %r', inbox, resp_payload)
95 async def follow_remote_actor(actor_uri):
96 logging.info('following: %r', actor_uri)
98 actor = await fetch_actor(actor_uri)
101 "@context": "https://www.w3.org/ns/activitystreams",
104 "object": actor['id'],
105 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
106 "actor": "https://{}/actor".format(AP_CONFIG['host'])
108 await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
111 async def unfollow_remote_actor(actor_uri):
112 logging.info('unfollowing: %r', actor_uri)
114 actor = await fetch_actor(actor_uri)
117 "@context": "https://www.w3.org/ns/activitystreams",
123 "actor": actor['id'],
124 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4())
126 "id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
127 "actor": "https://{}/actor".format(AP_CONFIG['host'])
129 await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
132 tag_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
133 def strip_html(data):
134 no_tags = tag_re.sub('', data)
135 return cgi.escape(no_tags)
138 async def handle_create(actor, data, request):
142 async def handle_follow(actor, data, request):
145 following = DATABASE.get('relay-list', [])
146 inbox = get_actor_inbox(actor)
148 if inbox not in following:
150 DATABASE['relay-list'] = following
153 "@context": "https://www.w3.org/ns/activitystreams",
156 "actor": "https://{}/actor".format(request.host),
158 # this is wrong per litepub, but mastodon < 2.4 is not compliant with that profile.
162 "object": "https://{}/actor".format(request.host),
166 "id": "https://{}/activities/{}".format(request.host, uuid.uuid4()),
169 asyncio.ensure_future(push_message_to_actor(actor, message, 'https://{}/actor#main-key'.format(request.host)))
171 if data['object'].endswith('/actor'):
172 asyncio.ensure_future(follow_remote_actor(actor['id']))
175 async def handle_undo(actor, data, request):
178 child = data['object']
179 if child['type'] == 'Follow':
180 following = DATABASE.get('relay-list', [])
182 inbox = get_actor_inbox(actor)
184 if inbox in following:
185 following.remove(inbox)
186 DATABASE['relay-list'] = following
188 if child['object'].endswith('/actor'):
189 await unfollow_remote_actor(actor['id'])
193 'Create': handle_create,
194 'Follow': handle_follow,
199 async def inbox(request):
200 data = await request.json()
202 if 'actor' not in data or not request['validated']:
203 raise aiohttp.web.HTTPUnauthorized(body='access denied', content_type='text/plain')
205 actor = await fetch_actor(data["actor"])
206 actor_uri = 'https://{}/actor'.format(request.host)
208 logging.debug(">> payload %r", data)
210 processor = processors.get(data['type'], None)
212 await processor(actor, data, request)
214 return aiohttp.web.Response(body=b'{}', content_type='application/activity+json')
217 app.router.add_post('/inbox', inbox)