From 2b9e1207f653f3d7bf6ed3d5c9bceef020cba26d Mon Sep 17 00:00:00 2001 From: kthouky Date: Tue, 8 Aug 2023 12:36:13 +0200 Subject: [PATCH] Adds censures Closes #15 --- fediseer/apis/v1/censures.py | 131 +++++++++++++++++++++++++++++++++ fediseer/classes/instance.py | 19 ++++- fediseer/database/functions.py | 39 +++++++++- 3 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 fediseer/apis/v1/censures.py diff --git a/fediseer/apis/v1/censures.py b/fediseer/apis/v1/censures.py new file mode 100644 index 0000000..0947c51 --- /dev/null +++ b/fediseer/apis/v1/censures.py @@ -0,0 +1,131 @@ +from fediseer.apis.v1.base import * +from fediseer.classes.instance import Censure + +class Censures(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, domains_csv): + '''Display all censures given out by one or more domains + You can pass a comma-separated list of domain names + and the results will be a set of all their censures together. + ''' + domains_list = domains_csv.split(',') + self.args = self.get_parser.parse_args() + instances = database.find_multiple_instance_by_domains(domains_list) + if not instances: + raise e.NotFound(f"No Instances found matching any of the provided domains. Have you remembered to register them?") + instance_details = [] + for instance in database.get_all_censured_instances_by_censuring_id([instance.id for instance in instances]): + 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 CensuredReceived(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 censures received 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 instance in database.get_all_censuring_instances_by_censured_id(instance.id): + 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("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, 'Not Guaranteed', models.response_model_error) + @api.response(404, 'Instance not registered', models.response_model_error) + def put(self, domain): + '''Censure an instance + A censure signifies a strong disapproval from your instance to how that instance is being run. + ''' + 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 admin 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 censure others.") + if instance.domain == domain: + raise e.BadRequest("You're a mad lad, but you can't censure 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, nodeinfo, admin_usernames = ensure_instance_registered(domain) + if not target_instance: + raise e.NotFound(f"Something went wrong trying to register this instance.") + if not target_instance: + raise e.BadRequest("Instance to censure not found") + if database.get_censure(target_instance.id,instance.id): + return {"message":'OK'}, 200 + new_censure = Censure( + censuring_id=instance.id, + censured_id=target_instance.id, + ) + db.session.add(new_censure) + db.session.commit() + logger.info(f"{instance.domain} Censured {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 censure + ''' + 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 admin 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 censure not found") + censure = database.get_censure(target_instance.id,instance.id) + if not censure: + return {"message":'OK'}, 200 + db.session.delete(censure) + db.session.commit() + logger.info(f"{instance.domain} Withdrew censure from {domain}") + return {"message":'Changed'}, 200 diff --git a/fediseer/classes/instance.py b/fediseer/classes/instance.py index ef7ea19..f47b74a 100644 --- a/fediseer/classes/instance.py +++ b/fediseer/classes/instance.py @@ -13,7 +13,7 @@ uuid_column_type = lambda: UUID(as_uuid=True) if not SQLITE_MODE else db.String( # 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): +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) @@ -48,11 +48,21 @@ class Endorsement(db.Model): endorsed_instance = db.relationship("Instance", back_populates="endorsements", foreign_keys=[endorsed_id]) created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) +class Censure(db.Model): + __tablename__ = "censures" + __table_args__ = (UniqueConstraint('censuring_id', 'censured_id', name='censures_censuring_id_censured_id'),) + id = db.Column(db.Integer, primary_key=True) + censuring_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False) + censuring_instance = db.relationship("Instance", back_populates="censures_given", foreign_keys=[censuring_id]) + censured_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False) + censured_instance = db.relationship("Instance", back_populates="censures_received", foreign_keys=[censured_id]) + created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + class Instance(db.Model): __tablename__ = "instances" - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True) domain = db.Column(db.String(255), 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) @@ -64,6 +74,8 @@ class Instance(db.Model): approvals = db.relationship("Endorsement", back_populates="approving_instance", cascade="all, delete-orphan", foreign_keys=[Endorsement.approving_id]) endorsements = db.relationship("Endorsement", back_populates="endorsed_instance", cascade="all, delete-orphan", foreign_keys=[Endorsement.endorsed_id]) + censures_given = db.relationship("Censure", back_populates="censuring_instance", cascade="all, delete-orphan", foreign_keys=[Censure.censuring_id]) + censures_received = db.relationship("Censure", back_populates="censured_instance", cascade="all, delete-orphan", foreign_keys=[Censure.censured_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]) @@ -108,8 +120,7 @@ class Instance(db.Model): def set_as_oprhan(self): self.oprhan_since = datetime.utcnow() db.session.commit() - + def unset_as_orphan(self): self.oprhan_since = None db.session.commit() - \ No newline at end of file diff --git a/fediseer/database/functions.py b/fediseer/database/functions.py index 0011718..114085d 100644 --- a/fediseer/database/functions.py +++ b/fediseer/database/functions.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import noload from fediseer.flask import db, SQLITE_MODE from fediseer.utils import hash_api_key from sqlalchemy.orm import joinedload -from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord +from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure from fediseer.classes.user import Claim, User def get_all_instances(min_endorsements = 0, min_guarantors = 1): @@ -63,6 +63,36 @@ def get_all_approving_instances_by_endorsed_id(endorsed_ids): ) return query.all() +def get_all_censured_instances_by_censuring_id(censuring_ids): + query = db.session.query( + Instance + ).outerjoin( + Instance.endorsements, + ).options( + joinedload(Instance.endorsements), + ).filter( + Censure.approving_id.in_(censuring_ids) + ).group_by( + Instance.id + ) + return query.all() + +def get_all_censuring_instances_by_censured_id(censured_ids): + query = db.session.query( + Instance + ).outerjoin( + Instance.approvals, + ).options( + joinedload(Instance.approvals), + ).filter( + Censure.endorsed_id.in_(censured_ids) + ).group_by( + Instance.id + ) + return query.all() + + + def get_all_guaranteed_instances_by_guarantor_id(guarantor_id): query = db.session.query( Instance @@ -180,6 +210,13 @@ def get_endorsement(instance_id, endorsing_instance_id): ) return query.first() +def get_censure(instance_id, censuring_instance_id): + query = Censure.query.filter_by( + censured_id=instance_id, + censuring_id=censuring_instance_id, + ) + return query.first() + def has_recent_endorsement(instance_id): query = Endorsement.query.filter( Endorsement.endorsed_id == instance_id,