aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorGravatar jesopo2019-09-15 10:43:46 +0100
committerGravatar jesopo2019-09-15 10:43:46 +0100
commit54ee1b35946b587e85c8995ed10b9d5eecab802d (patch)
tree9a28d65fd909bd74f848567b695c5fbb50b7658e /modules
parentremove unneeded "`"s (diff)
signature
re-merge fediverse an fediverse_server, so they can share utils
Diffstat (limited to 'modules')
-rw-r--r--modules/fediverse.py140
-rw-r--r--modules/fediverse/__init__.py87
-rw-r--r--modules/fediverse/activities.py26
-rw-r--r--modules/fediverse/ap_actor.py55
-rw-r--r--modules/fediverse/ap_utils.py56
-rw-r--r--modules/fediverse/security.py (renamed from modules/fediverse_server/security.py)14
-rw-r--r--modules/fediverse/server.py (renamed from modules/fediverse_server/__init__.py)100
7 files changed, 293 insertions, 185 deletions
diff --git a/modules/fediverse.py b/modules/fediverse.py
deleted file mode 100644
index bb464de0..00000000
--- a/modules/fediverse.py
+++ /dev/null
@@ -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))
diff --git a/modules/fediverse/__init__.py b/modules/fediverse/__init__.py
new file mode 100644
index 00000000..41e0bfe6
--- /dev/null
+++ b/modules/fediverse/__init__.py
@@ -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))
diff --git a/modules/fediverse/activities.py b/modules/fediverse/activities.py
new file mode 100644
index 00000000..8a7371c1
--- /dev/null
+++ b/modules/fediverse/activities.py
@@ -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"
diff --git a/modules/fediverse/ap_actor.py b/modules/fediverse/ap_actor.py
new file mode 100644
index 00000000..3f12de0d
--- /dev/null
+++ b/modules/fediverse/ap_actor.py
@@ -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)
+
diff --git a/modules/fediverse/ap_utils.py b/modules/fediverse/ap_utils.py
new file mode 100644
index 00000000..3e72d7ea
--- /dev/null
+++ b/modules/fediverse/ap_utils.py
@@ -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"]
+
diff --git a/modules/fediverse_server/security.py b/modules/fediverse/security.py
index 6ae75cd3..00fe8264 100644
--- a/modules/fediverse_server/security.py
+++ b/modules/fediverse/security.py
@@ -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()),
diff --git a/modules/fediverse_server/__init__.py b/modules/fediverse/server.py
index 82f03fc8..80e1654a 100644
--- a/modules/fediverse_server/__init__.py
+++ b/modules/fediverse/server.py
@@ -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):
-
- message = {
- "@context": "https://www.w3.org/ns/activitystreams",
- "type": "Announce",
- "to": [],
- "actor": "",
- "object": ""
- }
-
+ @utils.hook("received.command.fedifollow")
+ @utils.kwarg("min_args", 1)
+ @utils.kwarg("permission", "fediverse")
+ def fedi_follow(self, event):
+ pass
- 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