From 30bce74a69166d5ae741c817c4c2f5919b2479cc Mon Sep 17 00:00:00 2001 From: db0 Date: Thu, 22 Jun 2023 15:40:28 +0200 Subject: [PATCH] guarantees --- overseer/apis/v1/__init__.py | 8 +- overseer/apis/v1/base.py | 131 +--------------------------- overseer/apis/v1/endorsements.py | 7 ++ overseer/apis/v1/guarantees.py | 143 +++++++++++++++++++++++++++++++ overseer/apis/v1/whitelist.py | 135 +++++++++++++++++++++++++++++ overseer/classes/instance.py | 18 +++- overseer/database/functions.py | 72 +++++++++++++++- overseer/lemmy.py | 14 +-- 8 files changed, 384 insertions(+), 144 deletions(-) create mode 100644 overseer/apis/v1/guarantees.py create mode 100644 overseer/apis/v1/whitelist.py diff --git a/overseer/apis/v1/__init__.py b/overseer/apis/v1/__init__.py index 12d8744..9c3e348 100644 --- a/overseer/apis/v1/__init__.py +++ b/overseer/apis/v1/__init__.py @@ -1,9 +1,13 @@ 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 from overseer.apis.v1.base import api api.add_resource(base.Suspicions, "/instances") -api.add_resource(base.Whitelist, "/whitelist") -api.add_resource(base.WhitelistDomain, "/whitelist/") +api.add_resource(whitelist.Whitelist, "/whitelist") +api.add_resource(whitelist.WhitelistDomain, "/whitelist/") api.add_resource(endorsements.Endorsements, "/endorsements/") api.add_resource(endorsements.Approvals, "/approvals/") +api.add_resource(guarantees.Guarantors, "/guarantors/") +api.add_resource(guarantees.Guarantees, "/guarantees/") diff --git a/overseer/apis/v1/base.py b/overseer/apis/v1/base.py index 3955eea..844a3c1 100644 --- a/overseer/apis/v1/base.py +++ b/overseer/apis/v1/base.py @@ -8,7 +8,7 @@ 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 +from overseer.lemmy import pm_new_api_key, pm_instance from pythorhead import Lemmy api = Namespace('v1', 'API Version 1' ) @@ -50,132 +50,3 @@ class Suspicions(Resource): return {"domains": [instance["domain"] for instance in sus_instances]},200 return {"instances": sus_instances},200 - -class Whitelist(Resource): - get_parser = reqparse.RequestParser() - get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") - get_parser.add_argument("endorsements", required=False, default=0, type=int, help="Limit to this amount of endorsements of more", location="args") - get_parser.add_argument("guarantors", required=False, default=1, type=int, help="Limit to this amount of guarantors of more", location="args") - get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args") - get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args") - - @api.expect(get_parser) - @cache.cached(timeout=10, query_string=True) - @api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True) - def get(self): - '''A List with the details of all instances and their endorsements - ''' - self.args = self.get_parser.parse_args() - instance_details = [] - for instance in database.get_all_instances(self.args.endorsements,self.args.guarantors): - instance_details.append(instance.get_details()) - if self.args.csv: - return {"csv": ",".join([instance["domain"] for instance in instance_details])},200 - if self.args.domains: - return {"domains": [instance["domain"] for instance in instance_details]},200 - return {"instances": instance_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("domain", required=False, type=str, help="The instance domain. It MUST be alredy registered in https://overctrl.dbzer0.com", 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") - - - @api.expect(put_parser) - @api.marshal_with(models.response_model_instances, code=200, description='Instances') - @api.response(400, 'Bad Request', models.response_model_error) - def put(self): - '''Register a new instance to the overseer - An instance account has to exist in the overseer lemmy instance - That account will recieve the new API key via PM - ''' - self.args = self.put_parser.parse_args() - existing_instance = Instance.query.filter_by(domain=self.args.domain).first() - if existing_instance: - return existing_instance.get_details(),200 - requested_lemmy = Lemmy(f"https://{self.args.domain}") - site = requested_lemmy.site.get() - api_key = pm_new_api_key(self.args.domain) - if not api_key: - raise e.BadRequest("Failed to generate API Key") - new_instance = Instance( - domain=self.args.domain, - api_key=hash_api_key(api_key), - open_registrations=site["site_view"]["local_site"]["registration_mode"] == "open", - email_verify=site["site_view"]["local_site"]["require_email_verification"], - software=requested_lemmy.nodeinfo['software']['name'], - ) - new_instance.create() - return new_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("domain", required=False, type=str, help="The instance domain. It MUST be alredy registered in https://overctrl.dbzer0.com", location="json") - patch_parser.add_argument("regenerate_key", required=False, type=bool, help="If True, will PM a new api key to this instance", location="json") - - - @api.expect(patch_parser) - @api.marshal_with(models.response_model_instances, 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): - '''Regenerate API key for instance - ''' - 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?") - if self.args.regenerate_key: - new_key = pm_new_api_key(self.args.domain) - instance.api_key = hash_api_key(new_key) - db.session.commit() - return instance.get_details(),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("domain", required=False, type=str, help="The instance domain. It MUST be alredy registered in https://overctrl.dbzer0.com", location="json") - - - @api.expect(delete_parser) - @api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True) - @api.response(400, 'Bad Request', models.response_model_error) - @api.response(401, 'Invalid API Key', models.response_model_error) - @api.response(403, 'Forbidden', models.response_model_error) - def delete(self): - '''Delete instance from overseer - ''' - 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_authenticated_instance(self.args.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 self.args.domain == os.getenv('OVERSEER_LEMMY_DOMAIN'): - raise e.Forbidden("Cannot delete overseer control instance") - db.session.delete(instance) - db.session.commit() - logger.warning(f"{self.args.domain} deleted") - return {"message":'Changed'}, 200 - - - -class WhitelistDomain(Resource): - get_parser = reqparse.RequestParser() - get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") - - @api.expect(get_parser) - @cache.cached(timeout=10, query_string=True) - @api.marshal_with(models.response_model_instances, code=200, description='Instances') - def get(self, domain): - '''Display info about a specific instance - ''' - self.args = self.get_parser.parse_args() - instance = database.find_instance_by_domain(domain) - if not instance: - raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?") - return instance.get_details(),200 \ No newline at end of file diff --git a/overseer/apis/v1/endorsements.py b/overseer/apis/v1/endorsements.py index 08f1958..a3ae03f 100644 --- a/overseer/apis/v1/endorsements.py +++ b/overseer/apis/v1/endorsements.py @@ -77,7 +77,12 @@ class Endorsements(Resource): raise e.Forbidden("Only guaranteed instances can endorse others.") if instance.domain == domain: raise e.BadRequest("Nice try, but you can't endorse yourself.") + unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) + if not unbroken_chain: + 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 len(target_instance.guarantors) == 0: + raise e.Forbidden("Not Guaranteed instances can be endorsed. Please guarantee for them, or find someone who will.") if not target_instance: raise e.BadRequest("Instance to endorse not found") if database.get_endorsement(target_instance.id,instance.id): @@ -88,6 +93,7 @@ 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}") logger.info(f"{instance.domain} Endorsed {domain}") return {"message":'Changed'}, 200 @@ -118,5 +124,6 @@ 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") 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/overseer/apis/v1/guarantees.py new file mode 100644 index 0000000..5c2604d --- /dev/null +++ b/overseer/apis/v1/guarantees.py @@ -0,0 +1,143 @@ +from overseer.apis.v1.base import * +from overseer.classes.instance import Guarantee, Endorsement + +class Guarantors(Resource): + get_parser = reqparse.RequestParser() + get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") + get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args") + get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args") + + @api.expect(get_parser) + @cache.cached(timeout=10, query_string=True) + @api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True) + @api.response(404, 'Instance not registered', models.response_model_error) + def get(self, domain): + '''Display all guarantees given by a specific domain + ''' + self.args = self.get_parser.parse_args() + instance = database.find_instance_by_domain(domain) + if not instance: + raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?") + instance_details = [] + for guaranteed in database.get_all_guaranteed_instances_by_guarantor_id(instance.id): + instance_details.append(guaranteed.get_details()) + if self.args.csv: + return {"csv": ",".join([guaranteed["domain"] for guaranteed in instance_details])},200 + if self.args.domains: + return {"domains": [guaranteed["domain"] for guaranteed in instance_details]},200 + return {"instances": instance_details},200 + +class Guarantees(Resource): + get_parser = reqparse.RequestParser() + get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") + get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args") + get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args") + + @api.expect(get_parser) + @cache.cached(timeout=10, query_string=True) + @api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True) + @api.response(404, 'Instance not registered', models.response_model_error) + def get(self, domain): + '''Display all instances guaranteeing for this domain + ''' + self.args = self.get_parser.parse_args() + instance = database.find_instance_by_domain(domain) + if not instance: + raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?") + instance_details = [] + for guarantor in database.get_all_guarantor_instances_by_guaranteed_id(instance.id): + instance_details.append(guarantor.get_details()) + if self.args.csv: + return {"csv": ",".join([guarantor["domain"] for guarantor in instance_details])},200 + if self.args.domains: + return {"domains": [guarantor["domain"] for guarantor in instance_details]},200 + logger.debug(database.get_guarantor_chain(instance.id)) + return {"instances": instance_details},200 + + put_parser = reqparse.RequestParser() + put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') + put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") + + + @api.expect(put_parser) + @api.marshal_with(models.response_model_simple_response, code=200, description='Endorse Instance') + @api.response(400, 'Bad Request', models.response_model_error) + @api.response(401, 'Invalid API Key', models.response_model_error) + @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 + ''' + self.args = self.put_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.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") + if len(instance.guarantors) == 0: + raise e.Forbidden("Only guaranteed instances can guarantee others.") + unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) + if not unbroken_chain: + 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") + 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}") + new_guarantee = Guarantee( + guaranteed_id=target_instance.id, + guarantor_id=instance.id, + ) + db.session.add(new_guarantee) + # Guaranteed instances get their automatic first endorsement + new_endorsement = Endorsement( + approving_id=instance.id, + endorsed_id=target_instance.id, + ) + 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.") + logger.info(f"{instance.domain} Guaranteed for {domain}") + 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") + + @api.expect(delete_parser) + @api.marshal_with(models.response_model_simple_response, code=200, description='Withdraw Instance Endorsement') + @api.response(400, 'Bad Request', models.response_model_error) + @api.response(401, 'Invalid API Key', models.response_model_error) + @api.response(404, 'Instance not registered', models.response_model_error) + def delete(self,domain): + '''Withdraw an instance guarantee + ''' + self.args = self.delete_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.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") + target_instance = database.find_instance_by_domain(domain=domain) + if not target_instance: + raise e.BadRequest("Instance from which to withdraw endorsement not found") + # If API key matches the target domain, we assume they want to remove the guarantee added to them to allow another domain to guarantee them + if instance.id == target_instance.id: + guarantee = instance.get_guarantee() + else: + guarantee = database.get_guarantee(target_instance.id,instance.id) + if not guarantee: + return {"message":'OK'}, 200 + # Removing a guarantee removes the endorsement + endorsement = database.get_endorsement(target_instance.id,instance.id) + if endorsement: + db.session.delete(endorsement) + db.session.delete(guarantee) + db.session.commit() + pm_instance(target_instance.domain, f"Attention! You guarantor instance {instance.domain} has withdrawn their backing.\n\nIMPORTANT: All your endorsements and guarantees will be deleted unless you manage to find a new guarantor within 24hours!") + 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/overseer/apis/v1/whitelist.py new file mode 100644 index 0000000..8372ec4 --- /dev/null +++ b/overseer/apis/v1/whitelist.py @@ -0,0 +1,135 @@ +from overseer.apis.v1.base import * + +class Whitelist(Resource): + get_parser = reqparse.RequestParser() + get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") + get_parser.add_argument("endorsements", required=False, default=0, type=int, help="Limit to this amount of endorsements of more", location="args") + get_parser.add_argument("guarantors", required=False, default=1, type=int, help="Limit to this amount of guarantors of more", location="args") + get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args") + get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args") + + @api.expect(get_parser) + @cache.cached(timeout=10, query_string=True) + @api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True) + def get(self): + '''A List with the details of all instances and their endorsements + ''' + self.args = self.get_parser.parse_args() + instance_details = [] + for instance in database.get_all_instances(self.args.endorsements,self.args.guarantors): + instance_details.append(instance.get_details()) + if self.args.csv: + return {"csv": ",".join([instance["domain"] for instance in instance_details])},200 + if self.args.domains: + return {"domains": [instance["domain"] for instance in instance_details]},200 + return {"instances": instance_details},200 + + + +class WhitelistDomain(Resource): + get_parser = reqparse.RequestParser() + get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") + + @api.expect(get_parser) + @cache.cached(timeout=10, query_string=True) + @api.marshal_with(models.response_model_instances, code=200, description='Instances') + def get(self, domain): + '''Display info about a specific instance + ''' + self.args = self.get_parser.parse_args() + instance = database.find_instance_by_domain(domain) + if not instance: + raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?") + 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("guarantor", required=False, type=str, help="(Optiona) The domain of the guaranteeing instance. They will receive a PM to validate you", location="json") + + + @api.expect(put_parser) + @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 + That account will recieve the new API key via PM + ''' + self.args = self.put_parser.parse_args() + existing_instance = Instance.query.filter_by(domain=domain).first() + if existing_instance: + return existing_instance.get_details(),200 + if domain.endswith("test.dbzer0.com"): + requested_lemmy = Lemmy(f"https://{domain}") + requested_lemmy._requestor.nodeinfo = {"software":{"name":"lemmy"}} + site = {"site_view":{"local_site":{"require_email_verification": True,"registration_mode":"open"}}} + else: + requested_lemmy = Lemmy(f"https://{domain}") + site = requested_lemmy.site.get() + if not site: + raise e.BadRequest(f"Error encountered while polling domain {domain}. Please check it's running correctly") + api_key = pm_new_api_key(domain) + if not api_key: + raise e.BadRequest("Failed to generate API Key") + new_instance = Instance( + domain=domain, + api_key=hash_api_key(api_key), + open_registrations=site["site_view"]["local_site"]["registration_mode"] == "open", + email_verify=site["site_view"]["local_site"]["require_email_verification"], + software=requested_lemmy.nodeinfo['software']['name'], + ) + new_instance.create() + return new_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") + + + @api.expect(patch_parser) + @api.marshal_with(models.response_model_instances, 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): + '''Regenerate API key for instance + ''' + 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?") + if self.args.regenerate_key: + new_key = pm_new_api_key(domain) + instance.api_key = hash_api_key(new_key) + db.session.commit() + return instance.get_details(),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") + + + @api.expect(delete_parser) + @api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True) + @api.response(400, 'Bad Request', models.response_model_error) + @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 + ''' + 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_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") + db.session.delete(instance) + db.session.commit() + logger.warning(f"{domain} deleted") + return {"message":'Changed'}, 200 + diff --git a/overseer/classes/instance.py b/overseer/classes/instance.py index d5045fc..66f3cd4 100644 --- a/overseer/classes/instance.py +++ b/overseer/classes/instance.py @@ -54,7 +54,6 @@ class Instance(db.Model): db.session.commit() def get_details(self): - guarantor = self.get_guarantor() ret_dict = { "id": self.id, "domain": self.domain, @@ -62,12 +61,23 @@ class Instance(db.Model): "email_verify": self.email_verify, "endorsements": len(self.endorsements), "approvals": len(self.approvals), - "guarantor": guarantor.domain if guarantor else None, + "guarantor": self.get_guarantor_domain(), } return ret_dict - def get_guarantor(self): + + def get_guarantee(self): if len(self.guarantors) == 0: return None - guarantee = self.guarantors[0] + return self.guarantors[0] + + def get_guarantor(self): + guarantee = self.get_guarantee() + if not guarantee: + return None + return guarantee.guarantor_instance return Instance.query.filter_by(id=guarantee.guarantor_id).first() + + def get_guarantor_domain(self): + guarantor = self.get_guarantor() + return guarantor.domain if guarantor else None \ No newline at end of file diff --git a/overseer/database/functions.py b/overseer/database/functions.py index ead18bc..dda9012 100644 --- a/overseer/database/functions.py +++ b/overseer/database/functions.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import noload from overseer.flask import db, SQLITE_MODE from overseer.utils import hash_api_key from sqlalchemy.orm import joinedload -from overseer.classes.instance import Instance, Endorsement +from overseer.classes.instance import Instance, Endorsement, Guarantee def get_all_instances(min_endorsements = 0, min_guarantors = 1): query = db.session.query( @@ -57,6 +57,34 @@ def get_all_approving_instances_by_endorsed_id(endorsed_id): ) return query.all() +def get_all_guaranteed_instances_by_guarantor_id(guarantor_id): + query = db.session.query( + Instance + ).outerjoin( + Instance.guarantors, + ).options( + joinedload(Instance.guarantors), + ).filter( + Guarantee.guarantor_id == guarantor_id + ).group_by( + Instance.id + ) + return query.all() + +def get_all_guarantor_instances_by_guaranteed_id(guaranteed_id): + query = db.session.query( + Instance + ).outerjoin( + Instance.guarantees, + ).options( + joinedload(Instance.guarantees), + ).filter( + Guarantee.guaranteed_id == guaranteed_id + ).group_by( + Instance.id + ) + return query.all() + def find_instance_by_api_key(api_key): instance = Instance.query.filter_by(api_key=hash_api_key(api_key)).first() @@ -75,4 +103,44 @@ def get_endorsement(instance_id, endorsing_instance_id): endorsed_id=instance_id, approving_id=endorsing_instance_id, ) - return query.first() \ No newline at end of file + return query.first() + +def get_guarantee(instance_id, guarantor_id): + query = Guarantee.query.filter_by( + guaranteed_id=instance_id, + guarantor_id=guarantor_id, + ) + return query.first() + +def get_guarantor_chain(instance_id): + guarantors = set() + chainbreaker = None + query = Guarantee.query.filter_by( + guaranteed_id=instance_id, + ) + guarantor = query.first() + if not guarantor: + return set(),instance_id + guarantors.add(guarantor.guarantor_id) + if guarantor.guarantor_id != 0: + higher_guarantors, chainbreaker = get_guarantor_chain(guarantor.guarantor_id) + guarantors = higher_guarantors | guarantors + return guarantors,chainbreaker + +def has_unbroken_chain(instance_id): + guarantors, chainbreaker = get_guarantor_chain(instance_id) + if chainbreaker: + chainbreaker = Instance.query.filter_by(id=chainbreaker).first() + return 0 in guarantors,chainbreaker + +def get_guarantee_chain(instance_id): + query = Guarantee.query.filter_by( + guarantor_id=instance_id, + ) + guarantees = query.all() + if not guarantees: + return set() + guarantees_ids = set([g.guaranteed_id for g in guarantees]) + for gid in guarantees_ids: + guarantees_ids = guarantees_ids | get_guarantee_chain(gid) + return guarantees_ids diff --git a/overseer/lemmy.py b/overseer/lemmy.py index d779470..7c9fe24 100644 --- a/overseer/lemmy.py +++ b/overseer/lemmy.py @@ -10,15 +10,17 @@ if not _login: raise Exception("Failed to login to overctrl") overseer_lemmy_user = overctrl_lemmy.user.get(username=os.getenv('OVERSEER_LEMMY_USERNAME')) -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." +def pm_instance(domain: str, message: str): domain_username = domain.replace(".", "_") domain_user = overctrl_lemmy.user.get(username=domain_username) if not domain_user: raise e.BadRequest(f"Could not find domain user '{domain_username}'") - pm = overctrl_lemmy.private_message(pm_content,domain_user["person_view"]["person"]["id"]) - if not pm: + pm = overctrl_lemmy.private_message(message,domain_user["person_view"]["person"]["id"]) + return pm + +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." + if not pm_instance(domain, pm_content): raise e.BadRequest("API Key PM failed") return api_key -