re-merge fediverse an fediverse_server, so they can share utils

This commit is contained in:
jesopo 2019-09-15 10:43:46 +01:00
parent d7e3c69d30
commit 54ee1b3594
7 changed files with 293 additions and 185 deletions

View file

@ -1,140 +0,0 @@
import urllib.parse
from src import IRCBot, ModuleManager, utils
HOSTMETA = "https://%s/.well-known/host-meta"
WEBFINGER_DEFAULT = "https://%s/.well-known/webfinger?resource={uri}"
WEBFINGER_HEADERS = {"Accept": "application/jrd+json"}
ACTIVITY_TYPE = "application/activity+json"
ACTIVITY_HEADERS = {"Accept": ("application/ld+json; "
'profile="https://www.w3.org/ns/activitystreams"')}
USERAGENT = "BitBot (%s) Fediverse" % IRCBot.VERSION
def _parse_username(s):
username, _, instance = s.rpartition("@")
if username.startswith("@"):
username = username[1:]
if username and instance:
return username, instance
return None, None
def _format_username(username, instance):
return "@%s@%s" % (username, instance)
def _setting_parse(s):
username, instance = _parse_username(s)
if username and instance:
return _format_username(username, instance)
return None
@utils.export("set", utils.FunctionSetting(_setting_parse, "fediverse",
help="Set your fediverse account", example="@gargron@mastodon.social"))
class Module(ModuleManager.BaseModule):
_name = "Fedi"
@utils.hook("received.command.fediverse")
@utils.hook("received.command.fedi", alias_of="fediverse")
@utils.kwarg("help", "Get someone's latest toot")
@utils.kwarg("usage", "@<user>@<instance>")
def fedi(self, event):
account = None
if not event["args"]:
account = event["user"].get_setting("fediverse", None)
elif not "@" in event["args"]:
target = event["args_split"][0]
if event["server"].has_user_id(target):
target_user = event["server"].get_user(target)
account = target_user.get_setting("fediverse", None)
else:
account = event["args_split"][0]
username = None
instance = None
if account:
username, instance = _parse_username(account)
if not username or not instance:
raise utils.EventError("Please provide @<user>@<instance>")
hostmeta = utils.http.request(HOSTMETA % instance,
parse=True, check_content_type=False, useragent=USERAGENT)
webfinger_url = None
for item in hostmeta.data.find_all("link"):
if item["rel"] and item["rel"][0] == "lrdd":
webfinger_url = item["template"]
break
if webfinger_url == None:
self.log.debug("host-meta lookup failed for %s" % instance)
webfinger_url = WEBFINGER_DEFAULT % instance
webfinger_url = webfinger_url.replace("{uri}",
"acct:%s@%s" % (username, instance))
webfinger = utils.http.request(webfinger_url,
headers=WEBFINGER_HEADERS, json=True, useragent=USERAGENT)
activity_url = None
for link in webfinger.data["links"]:
if link["type"] == ACTIVITY_TYPE:
activity_url = link["href"]
break
if not activity_url:
raise utils.EventError("Failed to find user activity feed")
activity = utils.http.request(activity_url,
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
preferred_username = activity.data["preferredUsername"]
outbox_url = activity.data["outbox"]
outbox = utils.http.request(outbox_url, headers=ACTIVITY_HEADERS,
json=True, useragent=USERAGENT)
items = None
if "first" in outbox.data:
if type(outbox.data["first"]) == dict:
# pleroma
items = outbox.data["first"]["orderedItems"]
else:
# mastodon
first = utils.http.request(outbox.data["first"],
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
items = first.data["orderedItems"]
else:
items = outbox.data["orderedItems"]
if not items:
raise utils.EventError("No toots found")
first_item = items[0]
if first_item["type"] == "Announce":
retoot_url = first_item["object"]
retoot_instance = urllib.parse.urlparse(retoot_url).hostname
retoot = utils.http.request(retoot_url,
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
original_tooter_url = retoot.data["attributedTo"]
original_tooter = utils.http.request(original_tooter_url,
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
retooted_user = "@%s@%s" % (
original_tooter.data["preferredUsername"],
retoot_instance)
shorturl = self.exports.get_one("shorturl")(
event["server"], retoot_url)
retoot_content = utils.http.strip_html(
retoot.data["content"])
event["stdout"].write("%s (boost %s): %s - %s" % (
preferred_username, retooted_user, retoot_content,
shorturl))
elif first_item["type"] == "Create":
content = utils.http.strip_html(
first_item["object"]["content"])
url = first_item["object"]["id"]
shorturl = self.exports.get_one("shorturl")(
event["server"], url)
event["stdout"].write("%s: %s - %s" % (preferred_username,
content, shorturl))

View file

@ -0,0 +1,87 @@
import urllib.parse
from src import IRCBot, ModuleManager, utils
from . import ap_actor, ap_utils
def _format_username(username, instance):
return "@%s@%s" % (username, instance)
def _setting_parse(s):
username, instance = ap_utils.split_username(s)
if username and instance:
return _format_username(username, instance)
return None
@utils.export("set", utils.FunctionSetting(_setting_parse, "fediverse",
help="Set your fediverse account", example="@gargron@mastodon.social"))
class Module(ModuleManager.BaseModule):
_name = "Fedi"
@utils.hook("received.command.fediverse")
@utils.hook("received.command.fedi", alias_of="fediverse")
@utils.kwarg("help", "Get someone's latest toot")
@utils.kwarg("usage", "@<user>@<instance>")
def fedi(self, event):
account = None
if not event["args"]:
account = event["user"].get_setting("fediverse", None)
elif not "@" in event["args"]:
target = event["args_split"][0]
if event["server"].has_user_id(target):
target_user = event["server"].get_user(target)
account = target_user.get_setting("fediverse", None)
else:
account = event["args_split"][0]
username = None
instance = None
if account:
username, instance = ap_utils.split_username(account)
if not username or not instance:
raise utils.EventError("Please provide @<user>@<instance>")
actor_url = ap_utils.find_actor(username, instance)
if not actor_url:
raise utils.EventError("Failed to find actor")
actor = ap_actor.Actor(actor_url)
actor.load()
items = actor.outbox.load()
if not items:
raise utils.EventError("No toots found")
first_item = items[0]
if first_item["type"] == "Announce":
retoot_url = first_item["object"]
retoot_instance = urllib.parse.urlparse(retoot_url).hostname
retoot = utils.http.request(retoot_url,
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
original_tooter = ap_actor.Actor(retoot.data["attributedTo"])
original_tooter.load()
original_tooter = utils.http.request(original_tooter_url,
headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT)
retooted_user = "@%s@%s" % (original_tooter.username,
retoot_instance)
shorturl = self.exports.get_one("shorturl")(
event["server"], retoot_url)
retoot_content = utils.http.strip_html(
retoot.data["content"])
event["stdout"].write("%s (boost %s): %s - %s" % (
actor.username, retooted_user, retoot_content,
shorturl))
elif first_item["type"] == "Create":
content = utils.http.strip_html(
first_item["object"]["content"])
url = first_item["object"]["id"]
shorturl = self.exports.get_one("shorturl")(
event["server"], url)
event["stdout"].write("%s: %s - %s" % (actor.username,
content, shorturl))

View file

@ -0,0 +1,26 @@
from . import utils as ap_utils
class Activity(object):
_type = ""
def __init__(self, id, object):
self._id = id
self._object = object
def format(self, actor):
return {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": actor.url,
"id": self._id,
"object": self._object,
"type": self._type
}
class Follow(Activity):
_type = "Follow"
class Accept(Activity):
_type = "Accept"
class Create(Activity):
_type = "Create"
class Announce(Activity):
_type = "Announce"

View file

@ -0,0 +1,55 @@
import email.utils
from src import utils
from . import ap_utils
class Actor(object):
def __init__(self, url):
self.url = url
self.username = None
self.inbox = None
self.outbox = None
def load(self):
data = ap_utils.activity_request(self.url)
self.username = data["preferredUsername"]
self.inbox = Inbox(data["inbox"])
self.outbox = Outbox(data["outbox"])
class Outbox(object):
def __init__(self, url):
self._url = url
def load(self):
outbox = ap_utils.activity_request(self._url)
items = None
if "first" in outbox:
if type(outbox["first"]) == dict:
# pleroma
items = outbox["first"]["orderedItems"]
else:
# mastodon
first = ap_utils.activity_request(outbox["first"])
items = first["orderedItems"]
else:
items = outbox["orderedItems"]
return items
class Inbox(object):
def __init__(self, url):
self._url = url
def send(activity, private_key):
now = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
parts = urllib.parse.urlparse(self._url)
headers = [
["host", parts.netloc],
["date", now]
]
sign_headers = headers[:]
sign_headers.insert(0, ["(request-target)", "post %s" % parts.path])
signature = security.signature(private_key.key, sign_headers)
return ap_utils.request(self._url, activity.format(self),
method="POST", private_key=private_key)

View file

@ -0,0 +1,56 @@
from src import IRCBot, utils
LD_TYPE = ("application/ld+json; "
"profile=\"https://www.w3.org/ns/activitystreams\"")
JRD_TYPE = "application/jrd+json"
ACTIVITY_TYPE = "application/activity+json"
USERAGENT = "BitBot (%s) Fediverse" % IRCBot.VERSION
def split_username(s):
if s[0] == "@":
s = s[1:]
username, _, instance = s.partition("@")
if username and instance:
return username, instance
return None, None
def activity_request(url, data=None, method="GET", type=ACTIVITY_TYPE):
content_type = None
headers = {}
if method == "POST":
content_type = type
else:
headers = {"Accept": type}
request = utils.http.Request(url, headers=headers, useragent=USERAGENT,
content_type=content_type, data=data, json=True)
return utils.http.request(request).data
HOSTMETA_TEMPLATE = "https://%s/.well-known/host-meta"
WEBFINGER_TEMPLATE = "https://%s/.well-known/webfinger?resource={uri}"
def find_actor(username, instance):
hostmeta = HOSTMETA_TEMPLATE % instance
hostmeta_request = utils.http.Request(HOSTMETA_TEMPLATE % instance,
useragent=USERAGENT, parse=True, check_content_type=False)
hostmeta = utils.http.request(hostmeta_request)
webfinger_url = None
for item in hostmeta.data.find_all("link"):
if item["rel"] and item["rel"][0] == "lrdd":
webfinger_url = item["template"]
break
if not webfinger_url:
webfinger_url = WEBFINGER_TEMPLATE % instance
webfinger_url = webfinger_url.replace("{uri}",
"acct:%s@%s" % (username, instance), 1)
webfinger = activity_request(webfinger_url, type=JRD_TYPE)
actor_url = None
for link in webfinger["links"]:
if link["type"] == ACTIVITY_TYPE:
return link["href"]

View file

@ -7,20 +7,24 @@ SIGNATURE_FORMAT = (
"keyId=\"%s\",headers=\"%s\",signature=\"%s\",algorithm=\"rsa-sha256\"")
def private_key(key_filename: str) -> rsa.RSAPrivateKey:
def _private_key(key_filename: str) -> rsa.RSAPrivateKey:
with open(key_filename) as key_file:
return serialization.load_pem_private_key(
key_file.read(), password=None, backend=default_backend())
def signature(key: rsa.RSAPrivateKey, key_id: str,
headers: typing.List[typing.Tuple[str, str]]) -> str:
private_key = _private_key(key_filename)
class PrivateKey(object):
def __init__(self, filename, id):
self.key = _private_key(filename)
self.id = id
def signature(key: PrivateKey, headers: typing.List[typing.Tuple[str, str]]
) -> str:
sign_header_keys = " ".join(h[0] for h in headers)
sign_string_parts = ["%s: %s" % (k, v) for k, v in headers]
sign_string = "\n".join(sign_string_parts)
signature = private_key.sign(
signature = key.key.sign(
sign_string.encode("utf8"),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),

View file

@ -3,14 +3,9 @@
import base64, binascii, os, urllib.parse
from src import ModuleManager, utils
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
LD_TYPE = ("application/ld+json; "
"profile=\"https://www.w3.org/ns/activitystreams\"")
JRD_TYPE = "application/jrd+json"
ACTIVITY_TYPE = "application/activity+json"
from . import actor as ap_actor
from . import activities as ap_activities
from . import security as ap_security
ACTIVITY_SETTING_PREFIX = "ap-activity-"
@ -54,44 +49,41 @@ class Module(ModuleManager.BaseModule):
@utils.hook("received.command.toot")
@utils.kwarg("min_args", 1)
@utils.kwarg("permission", "toot")
@utils.kwarg("permission", "fediverse")
def toot(self, event):
activity_id = self._make_activity(event["args"])
event["stdout"].write("Sent toot %s" % activity_id)
def _federate_activity(self, activity_id, content, timestamp):
@utils.hook("received.command.fedifollow")
@utils.kwarg("min_args", 1)
@utils.kwarg("permission", "fediverse")
def fedi_follow(self, event):
pass
message = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Announce",
"to": [],
"actor": "",
"object": ""
}
def _federate(self, data):
def _toot(self, activity_id):
our_username, our_instance = self._ap_self()
key_id = self._ap_keyid_url(url_for, our_username)
now = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
content, timestamp = self.bot.get_setting(
"ap-activity-%s" % activity_id)
url_for = self.exports.get_one("url-for")
self_id = self._ap_self_url(url_for, our_username)
activity_url = self._ap_activity_url(url_for, activity_id)
key = security.private_key(self.bot.config["tls-certificate"])
object = {
"id": activity_url,
"type": "Note",
"published": timestamp,
"attributedTo": self_id,
"content": content,
"to": "https://www.w3.org/ns/activitystreams#Public"
}
activity = ap_activities.Create(activity_url, object)
for inbox in self._get_inboxes():
parts = urllib.parse.urlparse(inbox)
headers = [
["host", parts.netloc],
["date", now]
]
sign_headers = headers[:]
sign_headers.insert(0, ["(request-target)", "post %s" % parts.path])
private_key = self._private_key()
signature = security.signature(key, key_id, sign_headers)
data = ""
request = utils.http.Request(inbox, data=data, headers=headers,
content_type=ACTIVITY_TYPE, useragent="BitBot Fediverse")
utils.http.request()
for actor_url in self._get_actors():
actor = ap_actor.Actor(actor_url)
actor.load()
actor.inbox.send(activity, private_key)
def _ap_self(self):
our_username = self.bot.get_setting("fediverse", None)
@ -127,13 +119,13 @@ class Module(ModuleManager.BaseModule):
self_id = self._ap_self_url(event["url_for"], our_username)
event["response"].content_type = JRD_TYPE
event["response"].content_type = consts.JRD_TYPE
event["response"].write_json({
"aliases": [self_id],
"links": [{
"href": self_id,
"rel": "self",
"type": ACTIVITY_TYPE
"type": consts.ACTIVITY_TYPE
}],
"subject": "acct:%s" % resource
})
@ -157,7 +149,7 @@ class Module(ModuleManager.BaseModule):
with open(cert_filename) as cert_file:
cert = cert_file.read().strip()
event["response"].content_type = LD_TYPE
event["response"].content_type = consts.LD_TYPE
event["response"].write_json({
"@context": "https://www.w3.org/ns/activitystreams",
"id": self_id, "url": self_id,
@ -212,7 +204,7 @@ class Module(ModuleManager.BaseModule):
"type": "Create"
})
event["response"].content_type = LD_TYPE
event["response"].content_type = consts.LD_TYPE
event["response"].write_json({
"@context": "https://www.w3.org/ns/activitystreams",
"id": outbox,
@ -224,3 +216,31 @@ class Module(ModuleManager.BaseModule):
else:
event["response"].code = 404
def _private_key(self):
id = self._ap_keyid_url(url_for, our_username)
filename = security.private_key(self.bot.config["tls-certificate"])
return ap_security.PrivateKey(filename, id)
@utils.hook("api.post.ap-inbox")
@utils.kwarg("authenticated", False)
def ap_inbox(self, event):
data = json.loads(event["data"])
self_id = self._ap_self_url(event["url_for"], our_username)
if data["type"] == "Follow":
if data["object"] == self_id:
new_follower = data["actor"]
followers = set(self.bot.get_setting("fediverse-followers", []))
if not new_follower in followers:
followers.add(new_follower)
private_key = self._private_key()
actor = ap_actor.Actor(new_follower)
accept = ap_activities.Accept(data["id"], data)
actor.inbox.send(accept, private_key)
follow_id = "data:%s" % str(uuid.uuid4())
follow = ap_activities.Follow(follow_id, self_id)
actor.inbox.send(follow, private_key)
else:
event["response"].code = 404