pleroma-relay/viera/actor.py

198 lines
5.8 KiB
Python
Raw Normal View History

2018-08-10 21:14:22 +00:00
import aiohttp
2018-08-10 19:59:46 +00:00
import aiohttp.web
import logging
2018-08-11 01:36:24 +00:00
import uuid
import urllib.parse
import simplejson as json
import re
import cgi
2018-08-10 19:59:46 +00:00
from Crypto.PublicKey import RSA
from .database import DATABASE
# generate actor keys if not present
if "actorKeys" not in DATABASE:
logging.info("No actor keys present, generating 4096-bit RSA keypair.")
privkey = RSA.generate(4096)
pubkey = privkey.publickey()
DATABASE["actorKeys"] = {
"publicKey": pubkey.exportKey('PEM'),
"privateKey": privkey.exportKey('PEM')
}
2018-08-11 01:36:24 +00:00
PRIVKEY = RSA.importKey(DATABASE["actorKeys"]["privateKey"])
PUBKEY = PRIVKEY.publickey()
2018-08-17 23:09:24 +00:00
from . import app, CONFIG
2018-08-11 01:36:24 +00:00
from .remote_actor import fetch_actor
2018-08-10 19:59:46 +00:00
2018-08-17 23:09:24 +00:00
AP_CONFIG = CONFIG.get('ap', {'host': 'localhost'})
2018-08-10 19:59:46 +00:00
async def actor(request):
data = {
"@context": "https://www.w3.org/ns/activitystreams",
"endpoints": {
"sharedInbox": "https://{}/inbox".format(request.host)
},
"followers": "https://{}/followers".format(request.host),
2018-08-18 00:53:46 +00:00
"following": "https://{}/following".format(request.host),
2018-08-10 19:59:46 +00:00
"inbox": "https://{}/inbox".format(request.host),
"name": "Viera",
"type": "Application",
2018-08-10 20:14:51 +00:00
"id": "https://{}/actor".format(request.host),
2018-08-10 19:59:46 +00:00
"publicKey": {
"id": "https://{}/actor#main-key".format(request.host),
"owner": "https://{}/actor".format(request.host),
"publicKeyPem": DATABASE["actorKeys"]["publicKey"]
},
2018-08-10 20:14:51 +00:00
"summary": "Viera, the bot",
"preferredUsername": "viera",
"url": "https://{}/actor".format(request.host)
2018-08-10 19:59:46 +00:00
}
return aiohttp.web.json_response(data)
app.router.add_get('/actor', actor)
2018-08-11 01:36:24 +00:00
from .http_signatures import sign_headers
async def push_message_to_actor(actor, message, our_key_id):
url = urllib.parse.urlsplit(actor['inbox'])
# XXX: Digest
data = json.dumps(message)
headers = {
'(request-target)': 'post {}'.format(url.path),
'Content-Length': str(len(data)),
'Content-Type': 'application/activity+json',
'User-Agent': 'Viera'
}
headers['signature'] = sign_headers(headers, PRIVKEY, our_key_id)
2018-08-18 00:53:46 +00:00
logging.debug('%r', headers)
logging.debug('%r >> %r', actor['inbox'], message)
2018-08-11 01:36:24 +00:00
async with aiohttp.ClientSession() as session:
async with session.post(actor['inbox'], data=data, headers=headers) as resp:
pass
2018-08-17 23:09:24 +00:00
async def follow_remote_actor(actor_uri):
2018-08-18 00:53:46 +00:00
logging.info('following: %r', actor_uri)
2018-08-17 23:09:24 +00:00
actor = await fetch_actor(actor_uri)
message = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Follow",
"to": [actor['id']],
2018-08-18 00:53:46 +00:00
"object": actor['id'],
2018-08-17 23:09:24 +00:00
"id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
"actor": "https://{}/actor".format(AP_CONFIG['host'])
}
await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
2018-08-18 01:03:46 +00:00
async def unfollow_remote_actor(actor_uri):
logging.info('unfollowing: %r', actor_uri)
actor = await fetch_actor(actor_uri)
message = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Undo",
"to": [actor['id']],
"object": {
"type": "Follow",
"object": actor_uri,
"actor": actor['id'],
2018-08-18 01:04:18 +00:00
"id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4())
},
2018-08-18 01:03:46 +00:00
"id": "https://{}/activities/{}".format(AP_CONFIG['host'], uuid.uuid4()),
"actor": "https://{}/actor".format(AP_CONFIG['host'])
}
await push_message_to_actor(actor, message, "https://{}/actor#main-key".format(AP_CONFIG['host']))
2018-08-11 01:36:24 +00:00
tag_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
def strip_html(data):
no_tags = tag_re.sub('', data)
return cgi.escape(no_tags)
2018-08-17 23:01:44 +00:00
from .authreqs import check_reqs, get_irc_bot
2018-08-11 01:36:24 +00:00
async def handle_create(actor, data, request):
2018-08-17 23:01:44 +00:00
# for now, we only care about Notes
2018-08-18 00:53:46 +00:00
if data['object']['type'] != 'Note':
2018-08-17 23:01:44 +00:00
return
# strip the HTML if present
2018-08-11 01:36:24 +00:00
content = strip_html(data['object']['content']).split()
# check if the message is an authorization token for linking identities together
# if it is, then it's not a message we want to relay to IRC.
if check_reqs(content, actor):
return
2018-08-11 01:36:24 +00:00
2018-08-18 05:07:49 +00:00
# check that the message is public before relaying
public_uri = 'https://www.w3.org/ns/activitystreams#Public'
if public_uri in data.get('to', []) or public_uri in data.get('cc', []):
bot = get_irc_bot()
bot.relay_message(actor, data['object'], ' '.join(content))
2018-08-17 23:01:44 +00:00
2018-08-11 01:36:24 +00:00
async def handle_follow(actor, data, request):
message = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Accept",
"to": [actor["id"]],
2018-08-17 23:09:24 +00:00
"actor": "https://{}/actor".format(request.host),
2018-08-11 01:36:24 +00:00
# this is wrong per litepub, but mastodon < 2.4 is not compliant with that profile.
"object": {
"type": "Follow",
"id": data["id"],
"object": "https://{}/actor".format(request.host),
"actor": actor["id"]
},
"id": "https://{}/activities/{}".format(request.host, uuid.uuid4()),
}
await push_message_to_actor(actor, message, 'https://{}/actor#main-key'.format(request.host))
processors = {
'Create': handle_create,
'Follow': handle_follow
}
async def inbox(request):
2018-08-11 02:24:23 +00:00
data = await request.json()
2018-08-11 01:53:01 +00:00
if 'actor' not in data or not request['validated']:
raise aiohttp.web.HTTPUnauthorized(body='access denied', content_type='text/plain')
2018-08-11 01:36:24 +00:00
actor = await fetch_actor(data["actor"])
actor_uri = 'https://{}/actor'.format(request.host)
processor = processors.get(data['type'], None)
if processor:
await processor(actor, data, request)
return aiohttp.web.Response(body=b'{}', content_type='application/activity+json')
app.router.add_post('/inbox', inbox)