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-10 19:59:46 +00:00
|
|
|
from . import app
|
2018-08-11 01:36:24 +00:00
|
|
|
from .remote_actor import fetch_actor
|
|
|
|
|
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),
|
|
|
|
"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",
|
2018-08-11 15:21:08 +00:00
|
|
|
"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)
|
|
|
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
async with session.post(actor['inbox'], data=data, headers=headers) as resp:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
if data['object']['type'] != Note:
|
|
|
|
return
|
|
|
|
|
|
|
|
# strip the HTML if present
|
2018-08-11 01:36:24 +00:00
|
|
|
content = strip_html(data['object']['content']).split()
|
2018-08-17 22:36:05 +00:00
|
|
|
|
|
|
|
# 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-17 23:01:44 +00:00
|
|
|
# fetch our IRC bot
|
|
|
|
bot = get_irc_bot()
|
|
|
|
bot.relay_message(actor, data['object'], content)
|
|
|
|
|
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"]],
|
|
|
|
|
|
|
|
# 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)
|