diff --git a/.env_template b/.env_template
index 8830186..e4d19dc 100644
--- a/.env_template
+++ b/.env_template
@@ -1,7 +1,7 @@
-POSTGRES_URI="postgresql://postgres:ChangeMe@postgres.example.tld/overseer"
+POSTGRES_URI="postgresql://postgres:ChangeMe@postgres.example.tld/fediseer"
USE_SQLITE=0
-OVERSEER_LEMMY_DOMAIN="overctrl.example.tld"
-OVERSEER_LEMMY_USERNAME="overseer"
-OVERSEER_LEMMY_PASSWORD="LemmyPassword"
+FEDISEER_LEMMY_DOMAIN="fediseer.com"
+FEDISEER_LEMMY_USERNAME="fediseer"
+FEDISEER_LEMMY_PASSWORD="LemmyPassword"
ADMIN_API_KEY="Password"
secret_key="VerySecretKey"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index dfbe880..42dc2db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,10 +130,8 @@ dmypy.json
db/*
-test_commands.txt
-SQL_statements.txt
-horde.log
-horde*.bz2
-horde.db
-/.idea
-/boto3oeo.py
\ No newline at end of file
+fediseer.log
+fediseer*.bz2
+fediseer.db
+private.pem
+public.pem
\ No newline at end of file
diff --git a/README.md b/README.md
index b341484..ace0e0e 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-# Lemmy Overseer
+# Fediseer
-This service provides an REST API which can be used to retrieve various information about Lemmy instances, particularly focused on detecting and countering bad actors.
+This service provides an REST API which can be used to retrieve various information about Fediverse instances, particularly focused on detecting and countering bad actors.
It's reliant on the [Lemmy Fediverse Observer](https://lemmy.fediverse.observer/)
-The currently running instance is on https://overseer.dbzer0.com
+The currently running instance is on https://fediseer.com
-See devlog: https://dbzer0.com/blog/overseer-a-fediverse-chain-of-trust/
\ No newline at end of file
+See devlog: https://dbzer0.com/blog/fediseer-a-fediverse-chain-of-trust/
\ No newline at end of file
diff --git a/Untitled-1.json b/Untitled-1.json
new file mode 100644
index 0000000..03e44ab
--- /dev/null
+++ b/Untitled-1.json
@@ -0,0 +1,44 @@
+{
+ "id": "https://overctrl.dbzer0.com/activities/create/d50c9fe4-28a5-4de4-8421-8f806d9d0947",
+ "to": ["https://lemmy.dbzer0.com/u/db0"],
+ "type": "Create",
+ "actor": "https://overctrl.dbzer0.com/u/fediseer",
+ "object": {
+ "id": "https://overctrl.dbzer0.com/private_message/68",
+ "type": "ChatMessage",
+ "to": ["https://lemmy.dbzer0.com/u/db0"],
+ "source": {
+ "content": "password",
+ "mediaType": "text/markdown"
+ },
+ "attributedTo": "https: //overctrl.dbzer0.com/u/fediseer",
+ "content": "
password
\n",
+ "mediaType": "text/html",
+ "published": "2023-06-23T15: 57: 51.950327+00: 00"
+ },
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https: //w3id.org/security/v1",
+ {
+ "pt": "https://joinpeertube.org/ns#",
+ "sc": "http://schema.org/",
+ "lemmy": "https://join-lemmy.org/ns#",
+ "expires": "as:endTime",
+ "litepub": "http://litepub.social/ns#",
+ "language": "sc:inLanguage",
+ "stickied": "lemmy:stickied",
+ "sensitive": "as:sensitive",
+ "identifier": "sc:identifier",
+ "moderators": {
+ "@id": "lemmy:moderators",
+ "@type": "@id"
+ },
+ "removeData": "lemmy:removeData",
+ "ChatMessage": "litepub:ChatMessage",
+ "matrixUserId": "lemmy:matrixUserId",
+ "distinguished": "lemmy:distinguished",
+ "commentsEnabled": "pt:commentsEnabled",
+ "postingRestrictedToMods": "lemmy:postingRestrictedToMods"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/update_blacklist.py b/examples/update_blacklist.py
index 3bd6ee1..69208ec 100644
--- a/examples/update_blacklist.py
+++ b/examples/update_blacklist.py
@@ -21,7 +21,7 @@ if lemmy.log_in(USERNAME, PASSWORD) is False:
raise Exception("Could not log in to lemmy")
print("Fetching suspicions")
-sus = requests.get(f"https://overseer.dbzer0.com/api/v1/instances?activity_suspicion={ACTIVITY_SUSPICION}&domains=true", timeout=5).json()
+sus = requests.get(f"https://fediseer.com/api/v1/instances?activity_suspicion={ACTIVITY_SUSPICION}&domains=true", timeout=5).json()
defed = blacklist | set(sus["domains"])
print("Editing Defederation list")
ret = lemmy.site.edit(blocked_instances=list(defed))
diff --git a/overseer/__init__.py b/fediseer/__init__.py
similarity index 72%
rename from overseer/__init__.py
rename to fediseer/__init__.py
index 17c2a12..f76b146 100644
--- a/overseer/__init__.py
+++ b/fediseer/__init__.py
@@ -2,12 +2,12 @@ import os
import socket
from uuid import uuid4
-from overseer.logger import logger
-from overseer.flask import OVERSEER
-from overseer.routes import *
-from overseer.apis import apiv1
-from overseer.argparser import args
-from overseer.consts import OVERSEER_VERSION
+from fediseer.logger import logger
+from fediseer.flask import OVERSEER
+from fediseer.routes import *
+from fediseer.apis import apiv1
+from fediseer.argparser import args
+from fediseer.consts import OVERSEER_VERSION
OVERSEER.register_blueprint(apiv1)
diff --git a/overseer/apis/__init__.py b/fediseer/apis/__init__.py
similarity index 100%
rename from overseer/apis/__init__.py
rename to fediseer/apis/__init__.py
diff --git a/overseer/apis/apiv1.py b/fediseer/apis/apiv1.py
similarity index 88%
rename from overseer/apis/apiv1.py
rename to fediseer/apis/apiv1.py
index 6ccbcf0..0d235b1 100644
--- a/overseer/apis/apiv1.py
+++ b/fediseer/apis/apiv1.py
@@ -2,7 +2,7 @@ from flask import Blueprint
from flask_restx import Api
from importlib import import_module
-from overseer.apis.v1 import api as v1
+from fediseer.apis.v1 import api as v1
blueprint = Blueprint('apiv1', __name__, url_prefix='/api')
api = Api(blueprint,
diff --git a/overseer/apis/models/__init__.py b/fediseer/apis/models/__init__.py
similarity index 100%
rename from overseer/apis/models/__init__.py
rename to fediseer/apis/models/__init__.py
diff --git a/overseer/apis/models/v1.py b/fediseer/apis/models/v1.py
similarity index 100%
rename from overseer/apis/models/v1.py
rename to fediseer/apis/models/v1.py
diff --git a/overseer/apis/v1/__init__.py b/fediseer/apis/v1/__init__.py
similarity index 67%
rename from overseer/apis/v1/__init__.py
rename to fediseer/apis/v1/__init__.py
index 00bb60e..c999cb4 100644
--- a/overseer/apis/v1/__init__.py
+++ b/fediseer/apis/v1/__init__.py
@@ -1,9 +1,9 @@
-import overseer.apis.v1.base as base
-import overseer.apis.v1.whitelist as whitelist
-import overseer.apis.v1.endorsements as endorsements
-import overseer.apis.v1.guarantees as guarantees
-import overseer.apis.v1.activitypub as activitypub
-from overseer.apis.v1.base import api
+import fediseer.apis.v1.base as base
+import fediseer.apis.v1.whitelist as whitelist
+import fediseer.apis.v1.endorsements as endorsements
+import fediseer.apis.v1.guarantees as guarantees
+import fediseer.apis.v1.activitypub as activitypub
+from fediseer.apis.v1.base import api
api.add_resource(base.Suspicions, "/instances")
api.add_resource(activitypub.User, "/user/")
diff --git a/overseer/apis/v1/activitypub.py b/fediseer/apis/v1/activitypub.py
similarity index 64%
rename from overseer/apis/v1/activitypub.py
rename to fediseer/apis/v1/activitypub.py
index 4523387..8053235 100644
--- a/overseer/apis/v1/activitypub.py
+++ b/fediseer/apis/v1/activitypub.py
@@ -1,5 +1,5 @@
-from overseer.apis.v1.base import *
-from overseer.utils import get_nodeinfo
+from fediseer.apis.v1.base import *
+from fediseer.fediverse import get_nodeinfo
class User(Resource):
get_parser = reqparse.RequestParser()
@@ -10,28 +10,28 @@ class User(Resource):
'''User details
'''
self.args = self.get_parser.parse_args()
- if username != "overseer":
+ if username != "fediseer":
raise e.NotFound("User does not exist")
with open('public.pem', 'r') as file:
pubkey = file.read()
- overseer = {
+ fediseer = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Person",
- "id": "https://overseer.dbzer0.com/api/v1/user/overseer",
+ "id": "https://fediseer.com/api/v1/user/fediseer",
"type": "Person",
- "preferredUsername": "overseer",
- "inbox": "https://overseer.dbzer0.com/api/v1/inbox/overseer",
- "outbox": "https://overseer.dbzer0.com/api/v1/outbox/overseer",
+ "preferredUsername": "fediseer",
+ "inbox": "https://fediseer.com/api/v1/inbox/fediseer",
+ "outbox": "https://fediseer.com/api/v1/outbox/fediseer",
"publicKey": {
- "id": "https://overseer.dbzer0.com/api/v1/user/overseer#main-key",
- "owner": "https://overseer.dbzer0.com/api/v1/user/overseer",
+ "id": "https://fediseer.com/api/v1/user/fediseer#main-key",
+ "owner": "https://fediseer.com/api/v1/user/fediseer",
"publicKeyPem": pubkey
}
}
- return overseer,200
+ return fediseer,200
class Inbox(Resource):
post_parser = reqparse.RequestParser()
@@ -40,7 +40,7 @@ class Inbox(Resource):
def post(self, username):
'''User Inbox
'''
- if username != "overseer":
+ if username != "fediseer":
raise e.NotFound("User does not exist")
self.args = self.post_parser.parse_args()
json_payload = request.get_json()
diff --git a/overseer/apis/v1/base.py b/fediseer/apis/v1/base.py
similarity index 83%
rename from overseer/apis/v1/base.py
rename to fediseer/apis/v1/base.py
index 844a3c1..d430df8 100644
--- a/overseer/apis/v1/base.py
+++ b/fediseer/apis/v1/base.py
@@ -1,19 +1,19 @@
import os
from flask import request
from flask_restx import Namespace, Resource, reqparse
-from overseer.flask import cache, db
-from overseer.observer import retrieve_suspicious_instances
+from fediseer.flask import cache, db
+from fediseer.observer import retrieve_suspicious_instances
from loguru import logger
-from overseer.classes.instance import Instance
-from overseer.database import functions as database
-from overseer import exceptions as e
-from overseer.utils import hash_api_key
-from overseer.lemmy import pm_new_api_key, pm_instance
+from fediseer.classes.instance import Instance
+from fediseer.database import functions as database
+from fediseer import exceptions as e
+from fediseer.utils import hash_api_key
+from fediseer.messaging import activitypub_pm
from pythorhead import Lemmy
api = Namespace('v1', 'API Version 1' )
-from overseer.apis.models.v1 import Models
+from fediseer.apis.models.v1 import Models
models = Models(api)
diff --git a/overseer/apis/v1/endorsements.py b/fediseer/apis/v1/endorsements.py
similarity index 90%
rename from overseer/apis/v1/endorsements.py
rename to fediseer/apis/v1/endorsements.py
index 0093ef6..a3d510c 100644
--- a/overseer/apis/v1/endorsements.py
+++ b/fediseer/apis/v1/endorsements.py
@@ -1,5 +1,5 @@
-from overseer.apis.v1.base import *
-from overseer.classes.instance import Endorsement
+from fediseer.apis.v1.base import *
+from fediseer.classes.instance import Endorsement
class Approvals(Resource):
get_parser = reqparse.RequestParser()
@@ -93,7 +93,13 @@ class Endorsements(Resource):
)
db.session.add(new_endorsement)
db.session.commit()
- pm_instance(target_instance.domain, f"Your instance has just been endorsed by {instance.domain}")
+ if not database.has_recent_endorsement(target_instance.id):
+ activitypub_pm.pm_admins(
+ message=f"Your instance has just been endorsed by {instance.domain}",
+ domain=target_instance.domain,
+ software=target_instance.software,
+ instance=target_instance,
+ )
logger.info(f"{instance.domain} Endorsed {domain}")
return {"message":'Changed'}, 200
@@ -124,6 +130,11 @@ class Endorsements(Resource):
return {"message":'OK'}, 200
db.session.delete(endorsement)
db.session.commit()
- pm_instance(target_instance.domain, f"Oh now. {instance.domain} has just withdrawn the endorsement of your instance")
+ activitypub_pm.pm_admins(
+ message=f"Oh no. {instance.domain} has just withdrawn the endorsement of your instance",
+ domain=target_instance.domain,
+ software=target_instance.software,
+ instance=target_instance,
+ )
logger.info(f"{instance.domain} Withdrew endorsement from {domain}")
return {"message":'Changed'}, 200
\ No newline at end of file
diff --git a/overseer/apis/v1/guarantees.py b/fediseer/apis/v1/guarantees.py
similarity index 78%
rename from overseer/apis/v1/guarantees.py
rename to fediseer/apis/v1/guarantees.py
index c41b764..3cfd2fd 100644
--- a/overseer/apis/v1/guarantees.py
+++ b/fediseer/apis/v1/guarantees.py
@@ -1,5 +1,5 @@
-from overseer.apis.v1.base import *
-from overseer.classes.instance import Guarantee, Endorsement
+from fediseer.apis.v1.base import *
+from fediseer.classes.instance import Guarantee, Endorsement, RejectionRecord
class Guarantors(Resource):
get_parser = reqparse.RequestParser()
@@ -66,7 +66,7 @@ class Guarantees(Resource):
@api.response(403, 'Instance Not Guaranteed or Tartget instance Guaranteed by others', models.response_model_error)
@api.response(404, 'Instance not registered', models.response_model_error)
def put(self, domain):
- '''Endorse an instance
+ '''Guarantee an instance
'''
self.args = self.put_parser.parse_args()
if not self.args.apikey:
@@ -83,12 +83,12 @@ class Guarantees(Resource):
raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!")
target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance:
- raise e.BadRequest("Instance to endorse not found")
+ raise e.BadRequest("Instance to endorse not found")
if database.get_guarantee(target_instance.id,instance.id):
return {"message":'OK'}, 200
gdomain = target_instance.get_guarantor_domain()
if gdomain:
- raise e.Forbidden("Target instance already guaranteed by {gdomain}")
+ raise e.Forbidden(f"Target instance already guaranteed by {gdomain}")
new_guarantee = Guarantee(
guaranteed_id=target_instance.id,
guarantor_id=instance.id,
@@ -101,10 +101,20 @@ class Guarantees(Resource):
)
db.session.add(new_endorsement)
db.session.commit()
- pm_instance(target_instance.domain, f"Congratulations! Your instance has just been guaranteed by {instance.domain}. This also comes with your first endorsement.")
+ activitypub_pm.pm_admins(
+ message=f"Congratulations! Your instance has just been guaranteed by {instance.domain}. This also comes with your first endorsement.",
+ domain=target_instance.domain,
+ software=target_instance.software,
+ instance=target_instance,
+ )
orphan_ids = database.get_guarantee_chain(target_instance.id)
for orphan in database.get_instances_by_ids(orphan_ids):
- pm_instance(orphan.domain, f"Phew! You guarantor chain has been repaired as {instance.domain} has guaranteed for {domain}.")
+ activitypub_pm.pm_admins(
+ message=f"Phew! You guarantor chain has been repaired as {instance.domain} has guaranteed for {domain}.",
+ domain=orphan.domain,
+ software=orphan.software,
+ instance=orphan,
+ )
orphan.unset_as_orphan()
logger.info(f"{instance.domain} Guaranteed for {domain}")
return {"message":'Changed'}, 200
@@ -138,21 +148,40 @@ class Guarantees(Resource):
guarantee = database.get_guarantee(target_instance.id,instance.id)
if not guarantee:
return {"message":'OK'}, 200
+ if database.has_recent_rejection(target_instance.id,instance.id):
+ raise e.Forbidden("You cannot remove your guarantee from the same instance within 24 hours")
# Removing a guarantee removes the endorsement
- endorsement = database.get_endorsement(target_instance.id,instance.id)
- if endorsement:
+ endorsement = database.get_endorsement(target_instance.id,instance.id)
+ if endorsement:
db.session.delete(endorsement)
db.session.delete(guarantee)
+ rejection_record = database.get_rejection_record(instance.id,target_instance.id)
+ if rejection_record:
+ rejection_record.refresh()
+ else:
+ rejection = RejectionRecord(
+ rejected_id=target_instance.id,
+ rejector_id=instance.id,
+ )
+ db.session.add(rejection)
db.session.commit()
- pm_instance(target_instance.domain,
- f"Attention! You guarantor instance {instance.domain} has withdrawn their backing.\n\n"
- "IMPORTANT: You are still considered guaranteed for the next 24hours, but you cannot further endorse or guarantee others."
+ activitypub_pm.pm_admins(
+ message=f"Attention! You guarantor instance {instance.domain} has withdrawn their backing.\n\n"
+ "IMPORTANT: The instances you vouched for are still considered guaraneed but cannot guarantee or endorse others"
"If you find a new guarantor then your guarantees will be reactivated!.\n\n"
- "Note that if you do not find a guarantor within 7 days, all your endorsements will be removed."
+ "Note that if you do not find a guarantor within 7 days, all your guarantees and endorsements will be removed.",
+ domain=target_instance.domain,
+ software=target_instance.software,
+ instance=target_instance,
)
orphan_ids = database.get_guarantee_chain(target_instance.id)
for orphan in database.get_instances_by_ids(orphan_ids):
- pm_instance(orphan.domain, f"Attention! You guarantor chain has been broken because {instance.domain} has withdrawn their backing from {domain}.\n\nIMPORTANT: All your guarantees will be deleted unless the chain is repaired or you find a new guarantor within 24hours!")
+ activitypub_pm.pm_admins(
+ message=f"Attention! You guarantor chain has been broken because {instance.domain} has withdrawn their backing from {domain}.\n\nIMPORTANT: All your guarantees will be deleted unless the chain is repaired or you find a new guarantor within 24hours!",
+ domain=orphan.domain,
+ software=orphan.software,
+ instance=orphan,
+ )
orphan.set_as_oprhan()
logger.info(f"{instance.domain} Withdrew guarantee from {domain}")
return {"message":'Changed'}, 200
\ No newline at end of file
diff --git a/overseer/apis/v1/whitelist.py b/fediseer/apis/v1/whitelist.py
similarity index 61%
rename from overseer/apis/v1/whitelist.py
rename to fediseer/apis/v1/whitelist.py
index 8eb9741..d8e4a3b 100644
--- a/overseer/apis/v1/whitelist.py
+++ b/fediseer/apis/v1/whitelist.py
@@ -1,5 +1,8 @@
-from overseer.apis.v1.base import *
-from overseer.utils import get_nodeinfo
+from fediseer.apis.v1.base import *
+from fediseer.messaging import activitypub_pm
+from fediseer.fediverse import get_admin_for_software, get_nodeinfo
+from fediseer.classes.user import User, Claim
+from fediseer.consts import SUPPORTED_SOFTWARE
class Whitelist(Resource):
get_parser = reqparse.RequestParser()
@@ -38,14 +41,15 @@ class WhitelistDomain(Resource):
'''Display info about a specific instance
'''
self.args = self.get_parser.parse_args()
- instance = database.find_instance_by_domain(domain)
+ instance, nodeinfo, site, admin_usernames = self.ensure_instance_registered(domain)
if not instance:
- raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?")
+ raise e.NotFound(f"Something went wrong trying to register this instance.")
return instance.get_details(),200
put_parser = reqparse.RequestParser()
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
+ put_parser.add_argument("admin", required=False, type=str, help="The username of the admin who wants to register this domain", location="json")
put_parser.add_argument("guarantor", required=False, type=str, help="(Optiona) The domain of the guaranteeing instance. They will receive a PM to validate you", location="json")
@@ -53,64 +57,57 @@ class WhitelistDomain(Resource):
@api.marshal_with(models.response_model_instances, code=200, description='Instances')
@api.response(400, 'Bad Request', models.response_model_error)
def put(self, domain):
- '''Register a new instance to the overseer
- An instance account has to exist in the overseer lemmylemmy instance
+ '''Register a new instance to the fediseer
+ An instance account has to exist in the fediseer lemmylemmy instance
That account will recieve the new API key via PM
'''
self.args = self.put_parser.parse_args()
- existing_instance = database.find_instance_by_domain(domain)
- if existing_instance:
- return existing_instance.get_details(),200
+ if '@' in self.args.admin:
+ raise e.BadRequest("Please send the username without any @ signs or domains")
+ instance, nodeinfo, site, admin_usernames = self.ensure_instance_registered(domain)
guarantor_instance = None
if self.args.guarantor:
guarantor_instance = database.find_instance_by_domain(self.args.guarantor)
if not guarantor_instance:
raise e.BadRequest(f"Requested guarantor domain {self.args.guarantor} is not registered with the Overseer yet!")
- if domain.endswith("test.dbzer0.com"):
- requested_lemmy = Lemmy(f"https://{domain}")
- requested_lemmy._requestor.nodeinfo = {"software":{"name":"lemmy"}}
- open_registrations = False
- email_verify = True
- software = "lemmy"
- else:
- nodeinfo = get_nodeinfo(domain)
- if not nodeinfo:
- raise e.BadRequest(f"Error encountered while polling domain {domain}. Please check it's running correctly")
- software = nodeinfo["software"]["name"]
- if software == "lemmy":
- requested_lemmy = Lemmy(f"https://{domain}")
- site = requested_lemmy.site.get()
- if not site:
- raise e.BadRequest(f"Error encountered while polling lemmy domain {domain}. Please check it's running correctly")
- open_registrations = site["site_view"]["local_site"]["registration_mode"] == "open"
- email_verify = site["site_view"]["local_site"]["require_email_verification"]
- software = software
- else:
- open_registrations = nodeinfo["openRegistrations"]
- email_verify = False
- api_key = pm_new_api_key(domain)
+ if self.args.admin not in admin_usernames:
+ raise e.Forbidden(f"Only admins of that {instance.software} are allowed to claim it.")
+ existing_claim = database.find_claim(f"@{self.args.admin}@{domain}")
+ if existing_claim:
+ raise e.Forbidden(f"You have already claimed this instance as this admin. Please use the PATCH method to reset your API key.")
+ api_key = activitypub_pm.pm_new_api_key(domain, self.args.admin, instance.software)
if not api_key:
raise e.BadRequest("Failed to generate API Key")
- new_instance = Instance(
- domain=domain,
+ new_user = User(
api_key=hash_api_key(api_key),
- open_registrations=open_registrations,
- email_verify=email_verify,
- software=software,
+ account=f"@{self.args.admin}@{domain}",
+ username=self.args.admin,
)
- new_instance.create()
+ db.session.add(new_user)
+ db.session.commit()
+ new_claim = Claim(
+ user_id = new_user.id,
+ instance_id = instance.id,
+ )
+ db.session.add(new_claim)
+ db.session.commit()
if guarantor_instance:
- pm_instance(guarantor_instance.domain, f"New instance {domain} was just registered with the Overseer and have asked you to guarantee for them!")
- return new_instance.get_details(),200
+ activitypub_pm.pm_admins(
+ message=f"New instance {domain} was just registered with the Overseer and have asked you to guarantee for them!",
+ domain=guarantor_instance.domain,
+ software=guarantor_instance.software,
+ instance=guarantor_instance,
+ )
+ return instance.get_details(),200
patch_parser = reqparse.RequestParser()
patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
- patch_parser.add_argument("regenerate_key", required=False, type=bool, help="If True, will PM a new api key to this instance", location="json")
+ patch_parser.add_argument("regenerate_key", required=False, type=str, help="If a username is given, their API will be reset. This can be initiated by other instance admins or the fediseer.", location="json")
@api.expect(patch_parser)
- @api.marshal_with(models.response_model_instances, code=200, description='Instances', skip_none=True)
+ @api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Instance Not Registered', models.response_model_error)
def patch(self, domain):
@@ -118,19 +115,29 @@ class WhitelistDomain(Resource):
'''
self.args = self.patch_parser.parse_args()
if not self.args.apikey:
- raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
- instance = database.find_instance_by_api_key(self.args.apikey)
- if not instance:
- raise e.Forbidden(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
+ raise e.Unauthorized("You must provide the API key that was PM'd admin account")
+ user = database.find_user_by_api_key(self.args.apikey)
+ if not user:
+ raise e.Forbidden("You have not yet claimed an instance. Use the POST method to do so.")
+ instance = database.find_instance_by_user(user)
if self.args.regenerate_key:
- new_key = pm_new_api_key(domain)
- instance.api_key = hash_api_key(new_key)
+ requestor = None
+ if self.args.regenerate_key != user.username or user.username == "fediseer":
+ requestor = user.username
+ instance_to_reset = database.find_instance_by_account(f"@{self.args.regenerate_key}@{domain}")
+ if instance != instance_to_reset and user.username != "fediseer":
+ raise e.BadRequest("Only other admins or the fediseer can request API key reset for others.")
+ instance = instance_to_reset
+ user = database.find_user_by_account(f"@{self.args.regenerate_key}@{domain}")
+ new_key = activitypub_pm.pm_new_api_key(domain, self.args.regenerate_key, instance.software, requestor=requestor)
+ user.api_key = hash_api_key(new_key)
db.session.commit()
- return instance.get_details(),200
+ return {"message": "Changed"},200
delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
+ delete_parser.add_argument("username", required=False, type=str, help="(Not Implemented) Provide the username of another admin to remove their API key", location="json")
@api.expect(delete_parser)
@@ -139,18 +146,62 @@ class WhitelistDomain(Resource):
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Forbidden', models.response_model_error)
def delete(self, domain):
- '''Delete instance from overseer
+ '''Delete claim to instance
'''
+ return e.BadRequest("Not implemented")
self.args = self.patch_parser.parse_args()
if not self.args.apikey:
- raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
+ raise e.Unauthorized("You must provide the API key that was PM'd to your account")
instance = database.find_authenticated_instance(domain, self.args.apikey)
if not instance:
raise e.BadRequest(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
- if domain == os.getenv('OVERSEER_LEMMY_DOMAIN'):
- raise e.Forbidden("Cannot delete overseer control instance")
+ if domain == os.getenv('FEDISEER_LEMMY_DOMAIN'):
+ raise e.Forbidden("Cannot delete fediseer control instance")
db.session.delete(instance)
db.session.commit()
logger.warning(f"{domain} deleted")
return {"message":'Changed'}, 200
+ def ensure_instance_registered(self, domain):
+ if domain.endswith("test.dbzer0.com"):
+ # Fake instances for testing chain of trust
+ requested_lemmy = Lemmy(f"https://{domain}")
+ requested_lemmy._requestor.nodeinfo = {"software":{"name":"lemmy"}}
+ open_registrations = False
+ email_verify = True
+ software = "lemmy"
+ admin_usernames = ["db0"]
+ nodeinfo = get_nodeinfo("lemmy.dbzer0.com")
+ requested_lemmy = Lemmy(f"https://{domain}")
+ site = requested_lemmy.site.get()
+ else:
+ nodeinfo = get_nodeinfo(domain)
+ if not nodeinfo:
+ raise e.BadRequest(f"Error encountered while polling domain {domain}. Please check it's running correctly")
+ software = nodeinfo["software"]["name"]
+ if software not in SUPPORTED_SOFTWARE:
+ raise e.BadRequest(f"Fediverse software {software} not supported at this time")
+ if software == "lemmy":
+ requested_lemmy = Lemmy(f"https://{domain}")
+ site = requested_lemmy.site.get()
+ if not site:
+ raise e.BadRequest(f"Error encountered while polling lemmy domain {domain}. Please check it's running correctly")
+ open_registrations = site["site_view"]["local_site"]["registration_mode"] == "open"
+ email_verify = site["site_view"]["local_site"]["require_email_verification"]
+ software = software
+ admin_usernames = [a["person"]["name"] for a in site["admins"]]
+ else:
+ open_registrations = nodeinfo["openRegistrations"]
+ email_verify = False
+ admin_usernames = get_admin_for_software(software, domain)
+ instance = database.find_instance_by_domain(domain)
+ if instance:
+ return instance, nodeinfo, site, admin_usernames
+ new_instance = Instance(
+ domain=domain,
+ open_registrations=open_registrations,
+ email_verify=email_verify,
+ software=software,
+ )
+ new_instance.create()
+ return new_instance, nodeinfo, site, admin_usernames
diff --git a/overseer/argparser.py b/fediseer/argparser.py
similarity index 100%
rename from overseer/argparser.py
rename to fediseer/argparser.py
diff --git a/fediseer/classes/__init__.py b/fediseer/classes/__init__.py
new file mode 100644
index 0000000..38bd68c
--- /dev/null
+++ b/fediseer/classes/__init__.py
@@ -0,0 +1,49 @@
+import os
+from loguru import logger
+from fediseer.argparser import args
+from importlib import import_module
+from fediseer.flask import db, OVERSEER
+from fediseer.utils import hash_api_key
+
+# Importing for DB creation
+from fediseer.classes.instance import Instance, Guarantee
+from fediseer.classes.user import User, Claim
+import fediseer.classes.user
+
+with OVERSEER.app_context():
+
+ db.create_all()
+
+ admin_domain = os.getenv("FEDISEER_LEMMY_DOMAIN")
+ admin_instance = db.session.query(Instance).filter_by(domain=admin_domain).first()
+ if not admin_instance:
+ admin_instance = Instance(
+ id=0,
+ domain=admin_domain,
+ open_registrations=False,
+ email_verify=False,
+ software="fediseer",
+ )
+ admin_instance.create()
+ guarantee = Guarantee(
+ id=0,
+ guarantor_id = admin_instance.id,
+ guaranteed_id = admin_instance.id,
+ )
+ db.session.add(guarantee)
+ db.session.commit()
+ admin_user = User(
+ id=0,
+ account = "@fediseer@fediseer.com",
+ username = "fediseer",
+ api_key=hash_api_key(os.getenv("ADMIN_API_KEY")),
+ )
+ db.session.add(admin_user)
+ db.session.commit()
+ claim = Claim(
+ id=0,
+ user_id = admin_user.id,
+ instance_id = admin_instance.id
+ )
+ db.session.add(claim)
+ db.session.commit()
\ No newline at end of file
diff --git a/overseer/classes/instance.py b/fediseer/classes/instance.py
similarity index 71%
rename from overseer/classes/instance.py
rename to fediseer/classes/instance.py
index c88b21b..abc367c 100644
--- a/overseer/classes/instance.py
+++ b/fediseer/classes/instance.py
@@ -7,10 +7,26 @@ from sqlalchemy import Enum, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from loguru import logger
-from overseer.flask import db, SQLITE_MODE
+from fediseer.flask import db, SQLITE_MODE
uuid_column_type = lambda: UUID(as_uuid=True) if not SQLITE_MODE else db.String(36)
-
+
+# This is used to know when last time an instance removed their guarantee from another to prevent trolling/spamming
+# By someone adding/removing guarantees
+class RejectionRecord(db.Model):
+ __tablename__ = "rejection_records"
+ __table_args__ = (UniqueConstraint('rejector_id', 'rejected_id', name='endoresements_rejector_id_rejected_id'),)
+ id = db.Column(db.Integer, primary_key=True)
+ rejector_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
+ rejector_instance = db.relationship("Instance", back_populates="rejections", foreign_keys=[rejector_id])
+ rejected_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
+ rejected_instance = db.relationship("Instance", back_populates="rejectors", foreign_keys=[rejected_id])
+ created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ performed = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ def refresh(self):
+ self.performed = datetime.utcnow()
+
class Guarantee(db.Model):
__tablename__ = "guarantees"
@@ -38,7 +54,6 @@ class Instance(db.Model):
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(255), unique=True, nullable=False, index=True)
- api_key = db.Column(db.String(100), unique=True, nullable=False, index=True)
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
oprhan_since = db.Column(db.DateTime, nullable=True)
@@ -51,6 +66,9 @@ class Instance(db.Model):
endorsements = db.relationship("Endorsement", back_populates="endorsed_instance", cascade="all, delete-orphan", foreign_keys=[Endorsement.endorsed_id])
guarantees = db.relationship("Guarantee", back_populates="guarantor_instance", cascade="all, delete-orphan", foreign_keys=[Guarantee.guarantor_id])
guarantors = db.relationship("Guarantee", back_populates="guaranteed_instance", cascade="all, delete-orphan", foreign_keys=[Guarantee.guaranteed_id])
+ rejections = db.relationship("RejectionRecord", back_populates="rejector_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejector_id])
+ rejectors = db.relationship("RejectionRecord", back_populates="rejected_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejected_id])
+ admins = db.relationship("Claim", back_populates="instance", cascade="all, delete-orphan")
def create(self):
db.session.add(self)
diff --git a/overseer/classes/news.py b/fediseer/classes/news.py
similarity index 100%
rename from overseer/classes/news.py
rename to fediseer/classes/news.py
diff --git a/fediseer/classes/user.py b/fediseer/classes/user.py
new file mode 100644
index 0000000..c4d16cb
--- /dev/null
+++ b/fediseer/classes/user.py
@@ -0,0 +1,35 @@
+import uuid
+import os
+
+import dateutil.relativedelta
+from datetime import datetime
+from sqlalchemy import Enum, UniqueConstraint
+from sqlalchemy.dialects.postgresql import UUID
+
+from loguru import logger
+from fediseer.flask import db, SQLITE_MODE
+
+uuid_column_type = lambda: UUID(as_uuid=True) if not SQLITE_MODE else db.String(36)
+
+
+class Claim(db.Model):
+ __tablename__ = "claims"
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+ user = db.relationship("User", back_populates="claims")
+ instance_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
+ instance = db.relationship("Instance", back_populates="admins")
+ created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+
+class User(db.Model):
+ __tablename__ = "users"
+
+ id = db.Column(db.Integer, primary_key=True)
+ account = db.Column(db.String(255), unique=True, nullable=False, index=True)
+ username = db.Column(db.String(255), unique=False, nullable=False)
+ api_key = db.Column(db.String(100), unique=True, nullable=False, index=True)
+ created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ updated = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ claims = db.relationship("Claim", back_populates="user", cascade="all, delete-orphan")
diff --git a/fediseer/consts.py b/fediseer/consts.py
new file mode 100644
index 0000000..aa4d6e5
--- /dev/null
+++ b/fediseer/consts.py
@@ -0,0 +1,5 @@
+OVERSEER_VERSION = "0.5.0"
+SUPPORTED_SOFTWARE = [
+ "lemmy",
+ "mastodon",
+]
\ No newline at end of file
diff --git a/overseer/database/__init__.py b/fediseer/database/__init__.py
similarity index 100%
rename from overseer/database/__init__.py
rename to fediseer/database/__init__.py
diff --git a/overseer/database/functions.py b/fediseer/database/functions.py
similarity index 60%
rename from overseer/database/functions.py
rename to fediseer/database/functions.py
index 04fb7de..ccbedbf 100644
--- a/overseer/database/functions.py
+++ b/fediseer/database/functions.py
@@ -5,10 +5,11 @@ from loguru import logger
from datetime import datetime, timedelta
from sqlalchemy import func, or_, and_, not_, Boolean
from sqlalchemy.orm import noload
-from overseer.flask import db, SQLITE_MODE
-from overseer.utils import hash_api_key
+from fediseer.flask import db, SQLITE_MODE
+from fediseer.utils import hash_api_key
from sqlalchemy.orm import joinedload
-from overseer.classes.instance import Instance, Endorsement, Guarantee
+from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord
+from fediseer.classes.user import Claim, User
def get_all_instances(min_endorsements = 0, min_guarantors = 1):
query = db.session.query(
@@ -92,15 +93,78 @@ def get_all_guarantor_instances_by_guaranteed_id(guaranteed_id):
def find_instance_by_api_key(api_key):
- instance = Instance.query.filter_by(api_key=hash_api_key(api_key)).first()
+ instance = Instance.query.join(
+ Claim
+ ).join(
+ User
+ ).filter(
+ User.api_key == hash_api_key(api_key)
+ ).first()
return instance
+def find_instance_by_user(user):
+ instance = Instance.query.join(
+ Claim
+ ).join(
+ User
+ ).filter(
+ User.id == user.id
+ ).first()
+ return instance
+
+def find_instance_by_account(user_account):
+ instance = Instance.query.join(
+ Claim
+ ).join(
+ User
+ ).filter(
+ User.account == user_account
+ ).first()
+ return instance
+
+def find_admins_by_instance(instance):
+ users = User.query.join(
+ Claim
+ ).join(
+ Instance
+ ).filter(
+ Instance.id == instance.id
+ ).all()
+ return users
+
+def find_claim(admin_username):
+ claim = Claim.query.join(
+ User
+ ).filter(
+ User.account == admin_username
+ ).first()
+ return claim
+
+def find_user_by_api_key(api_key):
+ user = User.query.filter(
+ User.api_key == hash_api_key(api_key)
+ ).first()
+ return user
+
+def find_user_by_account(user_account):
+ user = User.query.filter(
+ User.account == user_account
+ ).first()
+ return user
+
def find_instance_by_domain(domain):
instance = Instance.query.filter_by(domain=domain).first()
return instance
def find_authenticated_instance(domain,api_key):
- instance = Instance.query.filter_by(domain=domain, api_key=hash_api_key(api_key)).first()
+ instance = Instance.query.join(
+ Claim
+ ).join(
+ User
+ ).filter(
+ User.api_key == hash_api_key(api_key),
+ Instance.domain ==domain,
+ ).first()
return instance
def get_endorsement(instance_id, endorsing_instance_id):
@@ -110,6 +174,35 @@ def get_endorsement(instance_id, endorsing_instance_id):
)
return query.first()
+def has_recent_endorsement(instance_id):
+ query = Endorsement.query.filter(
+ Endorsement.endorsed_id == instance_id,
+ Endorsement.created > datetime.utcnow() - timedelta(hours=1),
+ )
+ return query.first()
+
+def count_endorsements(instance_id):
+ query = Endorsement.query.filter_by(
+ endorsed_id=instance_id
+ )
+ return query.count()
+
+def has_recent_rejection(instance_id, rejector_id):
+ query = RejectionRecord.query.filter_by(
+ rejected_id=instance_id,
+ rejector_id=rejector_id,
+ ).filter(
+ RejectionRecord.performed > datetime.utcnow() - timedelta(hours=24)
+ )
+ return query.count() > 0
+
+def get_rejection_record(rejector_id, rejected_id):
+ query = RejectionRecord.query.filter_by(
+ rejected_id=rejected_id,
+ rejector_id=rejector_id,
+ )
+ return query.first()
+
def get_guarantee(instance_id, guarantor_id):
query = Guarantee.query.filter_by(
guaranteed_id=instance_id,
diff --git a/overseer/exceptions.py b/fediseer/exceptions.py
similarity index 100%
rename from overseer/exceptions.py
rename to fediseer/exceptions.py
diff --git a/fediseer/fediverse.py b/fediseer/fediverse.py
new file mode 100644
index 0000000..b70d166
--- /dev/null
+++ b/fediseer/fediverse.py
@@ -0,0 +1,37 @@
+import requests
+from loguru import logger
+from pythorhead import Lemmy
+
+def get_lemmy_admins(domain):
+ requested_lemmy = Lemmy(f"https://{domain}")
+ site = requested_lemmy.site.get()
+ if not site:
+ logger.warning(f"Error retrieving mastodon site info for {domain}")
+ return None
+ return [a["person"]["name"] for a in site["admins"]]
+
+def get_mastodon_admins(domain):
+ try:
+ site = requests(f"https://{domain}/api/v2/instance").json()
+ return [site["contact"]["account"]["username"]]
+ except Exception as err:
+ logger.warning(f"Error retrieving mastodon site info for {domain}")
+ return None
+
+def get_admin_for_software(software: str, domain: str):
+ software_map = {
+ "lemmy": get_lemmy_admins,
+ "mastodon": get_mastodon_admins,
+ }
+ if software not in software_map:
+ return None
+ return software_map[software](domain)
+
+
+def get_nodeinfo(domain):
+ try:
+ wellknown = requests.get(f"https://{domain}/.well-known/nodeinfo", timeout=2).json()
+ nodeinfo = requests.get(wellknown['links'][0]['href'], timeout=2).json()
+ return nodeinfo
+ except Exception as err:
+ return None
\ No newline at end of file
diff --git a/overseer/flask.py b/fediseer/flask.py
similarity index 88%
rename from overseer/flask.py
rename to fediseer/flask.py
index 44dea2a..1b72f47 100644
--- a/overseer/flask.py
+++ b/fediseer/flask.py
@@ -1,5 +1,5 @@
import os
-from flask import Flask
+from flask import Flask, redirect
from flask_caching import Cache
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_sqlalchemy import SQLAlchemy
@@ -13,7 +13,7 @@ SQLITE_MODE = os.getenv("USE_SQLITE", "0") == "1"
if SQLITE_MODE:
logger.warning("Using SQLite for database")
- OVERSEER.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///overseer.db"
+ OVERSEER.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///fediseer.db"
else:
OVERSEER.config["SQLALCHEMY_DATABASE_URI"] = os.getenv('POSTGRES_URI')
OVERSEER.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
@@ -38,3 +38,4 @@ if cache is None:
cache = Cache(config=cache_config)
cache.init_app(OVERSEER)
logger.init_warn("Flask Cache", status="SimpleCache")
+
diff --git a/overseer/lemmy.py b/fediseer/lemmy.py
similarity index 68%
rename from overseer/lemmy.py
rename to fediseer/lemmy.py
index 7c9fe24..f8c0562 100644
--- a/overseer/lemmy.py
+++ b/fediseer/lemmy.py
@@ -2,13 +2,13 @@ from pythorhead import Lemmy
from loguru import logger
import os
import secrets
-import overseer.exceptions as e
-
-overctrl_lemmy = Lemmy(f"https://{os.getenv('OVERSEER_LEMMY_DOMAIN')}")
-_login = overctrl_lemmy.log_in(os.getenv('OVERSEER_LEMMY_USERNAME'),os.getenv('OVERSEER_LEMMY_PASSWORD'))
+import fediseer.exceptions as e
+
+overctrl_lemmy = Lemmy(f"https://{os.getenv('FEDISEER_LEMMY_DOMAIN')}")
+_login = overctrl_lemmy.log_in(os.getenv('FEDISEER_LEMMY_USERNAME'),os.getenv('FEDISEER_LEMMY_PASSWORD'))
if not _login:
raise Exception("Failed to login to overctrl")
-overseer_lemmy_user = overctrl_lemmy.user.get(username=os.getenv('OVERSEER_LEMMY_USERNAME'))
+fediseer_lemmy_user = overctrl_lemmy.user.get(username=os.getenv('FEDISEER_LEMMY_USERNAME'))
def pm_instance(domain: str, message: str):
domain_username = domain.replace(".", "_")
@@ -20,7 +20,7 @@ def pm_instance(domain: str, message: str):
def pm_new_api_key(domain: str):
api_key = secrets.token_urlsafe(16)
- pm_content = f"The API Key for domain {domain} is\n\n{api_key}\n\nUse this to perform operations on the overseer."
+ pm_content = f"The API Key for domain {domain} is\n\n{api_key}\n\nUse this to perform operations on the fediseer."
if not pm_instance(domain, pm_content):
raise e.BadRequest("API Key PM failed")
return api_key
diff --git a/overseer/limiter.py b/fediseer/limiter.py
similarity index 89%
rename from overseer/limiter.py
rename to fediseer/limiter.py
index 6164ef4..8018188 100644
--- a/overseer/limiter.py
+++ b/fediseer/limiter.py
@@ -1,6 +1,6 @@
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
-from overseer.flask import OVERSEER
+from fediseer.flask import OVERSEER
from loguru import logger
limiter = None
diff --git a/overseer/logger.py b/fediseer/logger.py
similarity index 94%
rename from overseer/logger.py
rename to fediseer/logger.py
index 7e004c5..a34b183 100644
--- a/overseer/logger.py
+++ b/fediseer/logger.py
@@ -1,7 +1,7 @@
import sys
from functools import partialmethod
from loguru import logger
-from overseer.argparser import args
+from fediseer.argparser import args
STDOUT_LEVELS = ["GENERATION", "PROMPT"]
INIT_LEVELS = ["INIT", "INIT_OK", "INIT_WARN", "INIT_ERR"]
@@ -100,7 +100,7 @@ config = {
],
}
logger.configure(**config)
-logger.add("horde.log", retention="7 days", rotation="1d", compression="bz2", level=19)
+logger.add("fediseer.log", retention="7 days", rotation="1d", compression="bz2", level=19)
logger.disable("__main__")
logger.warning("disabled")
logger.enable("")
diff --git a/fediseer/messaging.py b/fediseer/messaging.py
new file mode 100644
index 0000000..63ecfa9
--- /dev/null
+++ b/fediseer/messaging.py
@@ -0,0 +1,137 @@
+import requests
+import json
+from datetime import datetime
+import OpenSSL.crypto
+import base64
+import hashlib
+import uuid
+import copy
+import os
+import secrets
+import fediseer.exceptions as e
+from pythorhead import Lemmy
+from loguru import logger
+from fediseer.database import functions as database
+from fediseer.consts import SUPPORTED_SOFTWARE
+from fediseer.fediverse import get_admin_for_software
+
+class ActivityPubPM:
+ private_key = None
+ def __init__(self):
+ with open('private.pem', 'rb') as file:
+ private_key_data = file.read()
+ self.private_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, private_key_data)
+ self.document_core = {
+ "type": "Create",
+ "actor": "https://fediseer.com/api/v1/user/fediseer",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https: //w3id.org/security/v1"
+ ],
+ "object": {
+ "attributedTo": "https://fediseer.com/api/v1/user/fediseer",
+ },
+ }
+
+ def send_pm_to_right_software(self, message, username, domain, software):
+ software_map = {
+ "lemmy": self.send_lemmy_pm,
+ "mastodon": self.send_mastodon_pm,
+ "fediseer": self.send_fediseer_pm,
+ }
+ return software_map[software](message, username, domain)
+
+ def send_fediseer_pm(self, message, username, domain):
+ document = copy.deepcopy(self.document_core)
+ document["to"] = [f"https://lemmy.dbzer0.com/u/db0"]
+ document["object"]["type"] = "ChatMessage"
+ document["object"]["mediaType"] = "text/html"
+ document["object"]["to"] = [f"https://lemmy.dbzer0.com/u/db0"]
+ document["object"]["source"] = {
+ "content": message,
+ "mediaType": "text/markdown",
+ }
+ return self.send_pm(document, message, domain)
+
+ def send_lemmy_pm(self, message, username, domain):
+ document = copy.deepcopy(self.document_core)
+ document["to"] = [f"https://{domain}/u/{username}"]
+ document["object"]["type"] = "ChatMessage"
+ document["object"]["mediaType"] = "text/html"
+ document["object"]["to"] = [f"https://{domain}/u/{username}"]
+ document["object"]["source"] = {
+ "content": message,
+ "mediaType": "text/markdown",
+ }
+ return self.send_pm(document, message, domain)
+
+ def send_mastodon_pm(self, message, username, domain):
+ document = copy.deepcopy(self.document_core)
+ document["object"]["type"] = "Note"
+ document["object"]["to"] = [f"https://{domain}/u/{username}"]
+ return self.send_pm(document, message, domain)
+
+ def send_pm(self, document, message, domain):
+ document["id"] = f"https://fediseer.com/{uuid.uuid4()}"
+ document["object"]["content"] = message
+ document["object"]["id"] = f"https://fediseer.com/{uuid.uuid4()}"
+ document["object"]["published"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+ document = json.dumps(document, indent=4)
+ digest = hashlib.sha256(document.encode('utf-8')).digest()
+ encoded_digest = base64.b64encode(digest).decode('utf-8')
+ digest_header = "SHA-256=" + encoded_digest
+ date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
+
+ signed_string = f"(request-target): post /inbox\nhost: {domain}\ndate: {date}\ndigest: {digest_header}"
+ signature = OpenSSL.crypto.sign(self.private_key, signed_string.encode('utf-8'), 'sha256')
+ encoded_signature = base64.b64encode(signature).decode('utf-8')
+
+ header = f'keyId="https://fediseer.com/api/v1/user/fediseer",headers="(request-target) host date digest",signature="{encoded_signature}"'
+ headers = {
+ 'Host': domain,
+ 'Date': date,
+ 'Signature': header,
+ 'Digest': digest_header,
+ 'Content-Type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
+ }
+ url = f"https://{domain}/inbox"
+ response = requests.post(url, data=document, headers=headers)
+ return response.ok
+
+ def pm_new_api_key(self, domain: str, username: str, software: str, requestor = None):
+ api_key = secrets.token_urlsafe(16)
+ if requestor:
+ pm_content = f"user '{requestor}' has initiated an API Key reset for your domain {domain} on the [Fediseer](https://fediseer.com)\n\nThe new API key is\n\n{api_key}"
+ else:
+ pm_content = f"Your API Key for domain {domain} is\n\n{api_key}\n\nUse this to perform operations on the [Fediseer](https://fediseer.com)."
+ if not self.send_pm_to_right_software(
+ message=pm_content,
+ username=username,
+ domain=domain,
+ software=software
+ ):
+ raise e.BadRequest("API Key PM failed")
+ return api_key
+
+
+ def pm_admins(self, message: str, domain: str, software: str, instance):
+ if software not in SUPPORTED_SOFTWARE:
+ return None
+ admins = database.find_admins_by_instance(instance)
+ if not admins:
+ admins = get_admin_for_software(software, domain)
+ else:
+ admins = [a.username for a in admins]
+ if not admins:
+ raise e.BadRequest(f"Could not determine admins for {domain}")
+ for admin_username in admins:
+ if not self.send_pm_to_right_software(
+ message=message,
+ username=admin_username,
+ domain=domain,
+ software=software
+ ):
+ raise e.BadRequest("Admin PM Failed")
+
+
+activitypub_pm = ActivityPubPM()
diff --git a/overseer/observer.py b/fediseer/observer.py
similarity index 100%
rename from overseer/observer.py
rename to fediseer/observer.py
diff --git a/overseer/routes.py b/fediseer/routes.py
similarity index 79%
rename from overseer/routes.py
rename to fediseer/routes.py
index 6ce4c9f..9bb4b3a 100644
--- a/overseer/routes.py
+++ b/fediseer/routes.py
@@ -1,13 +1,14 @@
from flask import render_template, redirect, url_for, request
from markdown import markdown
from loguru import logger
-from overseer.flask import OVERSEER
+from fediseer.flask import OVERSEER
+import fediseer.exceptions as e
@logger.catch(reraise=True)
@OVERSEER.route('/')
# @cache.cached(timeout=300)
def index():
- with open(f'overseer/templates/index.md') as index_file:
+ with open(f'fediseer/templates/index.md') as index_file:
index = index_file.read()
findex = index.format()
@@ -31,23 +32,21 @@ def index():
"""
return(head + markdown(findex))
-
@logger.catch(reraise=True)
@OVERSEER.route('/.well-known/webfinger')
def wellknown_redirect():
query_string = request.query_string.decode()
if not query_string:
return {"message":"No user specified"},400
- if query_string != "resource=acct:overseer@overseer.dbzer0.com":
+ if query_string != "resource=acct:fediseer@fediseer.com":
return {"message":"User does not exist"},404
webfinger = {
- "subject": "acct:overseer@overseer.dbzer0.com",
-
+ "subject": "acct:fediseer@fediseer.com",
"links": [
{
"rel": "self",
"type": "application/activity+json",
- "href": "https://overseer.dbzer0.com/api/v1/user/overseer"
+ "href": "https://fediseer.com/api/v1/user/fediseer"
}
]
}
diff --git a/overseer/templates/index.md b/fediseer/templates/index.md
similarity index 81%
rename from overseer/templates/index.md
rename to fediseer/templates/index.md
index 722b209..8683b3e 100644
--- a/overseer/templates/index.md
+++ b/fediseer/templates/index.md
@@ -1,8 +1,8 @@
# Lemmy Overseer
-This is a [FOSS service](https://github.com/db0/lemmy-overseer) to help Lemmy instances detect and avoid suspcicious instances
+This is a [FOSS service](https://github.com/db0/lemmy-fediseer) to help Lemmy instances detect and avoid suspcicious instances
-[Release Devlog](https://dbzer0.com/blog/overseer-a-fediverse-chain-of-trust/)
+[Release Devlog](https://dbzer0.com/blog/fediseer-a-fediverse-chain-of-trust/)
## Scope
diff --git a/overseer/utils.py b/fediseer/utils.py
similarity index 86%
rename from overseer/utils.py
rename to fediseer/utils.py
index 06c879e..e54e107 100644
--- a/overseer/utils.py
+++ b/fediseer/utils.py
@@ -9,9 +9,7 @@ import json
from datetime import datetime
import dateutil.relativedelta
from loguru import logger
-from overseer.flask import SQLITE_MODE
-import requests
-
+from fediseer.flask import SQLITE_MODE
random.seed(random.SystemRandom().randint(0, 2**32 - 1))
@@ -102,11 +100,3 @@ def validate_regex(regex_string):
except:
return False
return True
-
-def get_nodeinfo(domain):
- try:
- wellknown = requests.get(f"https://{domain}/.well-known/nodeinfo", timeout=2).json()
- nodeinfo = requests.get(wellknown['links'][0]['href'], timeout=2).json()
- return nodeinfo
- except Exception as err:
- return None
\ No newline at end of file
diff --git a/overseer/classes/__init__.py b/overseer/classes/__init__.py
deleted file mode 100644
index 82a7f18..0000000
--- a/overseer/classes/__init__.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import os
-from loguru import logger
-from overseer.argparser import args
-from importlib import import_module
-from overseer.flask import db, OVERSEER
-from overseer.utils import hash_api_key
-
-# Importing for DB creation
-from overseer.classes.instance import Instance, Guarantee
-
-with OVERSEER.app_context():
-
- db.create_all()
-
- admin_domain = os.getenv("OVERSEER_LEMMY_DOMAIN")
- admin = db.session.query(Instance).filter_by(domain=admin_domain).first()
- if not admin:
- admin = Instance(
- id=0,
- domain=admin_domain,
- api_key=hash_api_key(os.getenv("ADMIN_API_KEY")),
- open_registrations=False,
- email_verify=False,
- software="lemmy",
- )
- admin.create()
- guarantee = Guarantee(
- guarantor_id = admin.id,
- guaranteed_id = admin.id,
- )
- db.session.add(guarantee)
- db.session.commit()
\ No newline at end of file
diff --git a/overseer/consts.py b/overseer/consts.py
deleted file mode 100644
index 47b796e..0000000
--- a/overseer/consts.py
+++ /dev/null
@@ -1 +0,0 @@
-OVERSEER_VERSION = "0.0.1"
diff --git a/quickrun.py b/quickrun.py
index b7769f7..9fa5bc5 100644
--- a/quickrun.py
+++ b/quickrun.py
@@ -1,5 +1,5 @@
import json
-from overseer.observer import retrieve_suspicious_instances
+from fediseer.observer import retrieve_suspicious_instances
sus = retrieve_suspicious_instances(20)
if sus:
diff --git a/server.py b/server.py
index 20723bd..bc8454a 100644
--- a/server.py
+++ b/server.py
@@ -4,8 +4,8 @@ import logging
load_dotenv()
-from overseer.argparser import args
-from overseer.flask import OVERSEER
+from fediseer.argparser import args
+from fediseer.flask import OVERSEER
from loguru import logger
if __name__ == "__main__":
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..c6cdd9c
--- /dev/null
+++ b/test.py
@@ -0,0 +1,41 @@
+import requests
+import json
+from datetime import datetime
+import OpenSSL.crypto
+import base64
+import hashlib
+import sys
+import uuid
+
+with open('lemmy-hello-world.json', 'r') as file:
+ document = json.loads(file.read())
+document["id"] = f"https://fediseer.com/{uuid.uuid4()}"
+document["object"]["id"] = f"https://fediseer.com/{uuid.uuid4()}"
+document["object"]["published"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+document = json.dumps(document, indent=4)
+print(document)
+digest = hashlib.sha256(document.encode('utf-8')).digest()
+encoded_digest = base64.b64encode(digest).decode('utf-8')
+digest_header = "SHA-256=" + encoded_digest
+date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
+
+with open('private.pem', 'rb') as file:
+ private_key_data = file.read()
+ private_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, private_key_data)
+
+signed_string = f"(request-target): post /inbox\nhost: overctrl.dbzer0.com\ndate: {date}\ndigest: {digest_header}"
+signature = OpenSSL.crypto.sign(private_key, signed_string.encode('utf-8'), 'sha256')
+encoded_signature = base64.b64encode(signature).decode('utf-8')
+
+header = f'keyId="https://fediseer.com/api/v1/user/fediseer",headers="(request-target) host date digest",signature="{encoded_signature}"'
+headers = {
+ 'Host': 'overctrl.dbzer0.com',
+ 'Date': date,
+ 'Signature': header,
+ 'Digest': digest_header,
+ 'Content-Type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
+}
+url = 'https://overctrl.dbzer0.com/inbox'
+response = requests.post(url, data=document, headers=headers)
+print('Response Status:', response.status_code)
+print('Response Body:', response.text)