feat: Rebuttals
parent
4ab98ec12b
commit
d380f34364
|
@ -62,6 +62,7 @@ class Models:
|
|||
self.response_model_instances_censured = api.inherit('CensuredInstanceDetails', self.response_model_instances, {
|
||||
'censure_reasons': fields.List(fields.String(description="The reasons instances have given for censuring this instance")),
|
||||
'censure_evidence': fields.List(fields.String(description="Evidence justifying this censure, typically should be one or more URLs.")),
|
||||
'rebuttal': fields.List(fields.String(description="Counter argument by the target instance.", example="Nuh uh!")),
|
||||
'censure_count': fields.Integer(description="The amount of censures this instance has received from the reference instances"),
|
||||
})
|
||||
self.response_model_model_Censures_get = api.model('CensuredInstances', {
|
||||
|
@ -80,6 +81,7 @@ class Models:
|
|||
self.response_model_dubious_instances = api.inherit('DubiousInstanceDetails', self.response_model_instances, {
|
||||
'hesitation_reasons': fields.List(fields.String(description="The reasons instances have given for hesitating against this instance")),
|
||||
'hesitation_evidence': fields.List(fields.String(description="Evidence justifying this hesitation, typically should be one or more URLs.")),
|
||||
'rebuttal': fields.List(fields.String(description="Counter argument by the target instance.", example="Nuh uh!")),
|
||||
'hesitation_count': fields.Integer(description="The amount of hesitations this instance has received from the reference instances"),
|
||||
})
|
||||
self.response_model_model_Hesitations_get = api.model('DubiousInstances', {
|
||||
|
@ -94,6 +96,9 @@ class Models:
|
|||
'reason': fields.String(required=False, description="The reason for this censure/hesitation. No profanity or hate speech allowed!", example="csam"),
|
||||
'evidence': fields.String(required=False, description="The evidence for this censure/hesitation. Typically URL but can be a long form of anything you feel appropriate.", example="https://link.to/your/evidence", max_length=1000),
|
||||
})
|
||||
self.input_rebuttals_modify = api.model('ModifyRebuttals', {
|
||||
'rebuttal': fields.String(required=False, description="The counter-argument for this censure/hesitation.", example="Nuh uh!", max_length=1000),
|
||||
})
|
||||
self.response_model_api_key_reset = api.model('ApiKeyReset', {
|
||||
"message": fields.String(default='OK',required=True, description="The result of this operation."),
|
||||
"new_key": fields.String(default=None,required=False, description="The new API key"),
|
||||
|
|
|
@ -4,6 +4,7 @@ import fediseer.apis.v1.solicitations as solicitations
|
|||
import fediseer.apis.v1.endorsements as endorsements
|
||||
import fediseer.apis.v1.censures as censures
|
||||
import fediseer.apis.v1.hesitations as hesitations
|
||||
import fediseer.apis.v1.rebuttals as rebuttals
|
||||
import fediseer.apis.v1.guarantees as guarantees
|
||||
import fediseer.apis.v1.activitypub as activitypub
|
||||
import fediseer.apis.v1.badges as badges
|
||||
|
@ -27,6 +28,7 @@ api.add_resource(endorsements.BatchEndorsements, "/batch/endorsements")
|
|||
api.add_resource(censures.Censures, "/censures/<string:domain>")
|
||||
api.add_resource(censures.CensuresGiven, "/censures_given/<string:domains_csv>")
|
||||
api.add_resource(censures.BatchCensures, "/batch/censures")
|
||||
api.add_resource(rebuttals.Rebuttals, "/rebuttals/<string:domain>")
|
||||
api.add_resource(hesitations.Hesitations, "/hesitations/<string:domain>")
|
||||
api.add_resource(hesitations.HesitationsGiven, "/hesitations_given/<string:domains_csv>")
|
||||
api.add_resource(hesitations.BatchHesitations, "/batch/hesitations")
|
||||
|
|
|
@ -154,6 +154,7 @@ class Censures(Resource):
|
|||
if p_instance != get_instance:
|
||||
continue
|
||||
instances.append(p_instance)
|
||||
rebuttals = database.get_all_rebuttals_from_source_instance_id(instance.id,[c.id for c in instances])
|
||||
for c_instance in instances:
|
||||
censures = database.get_all_censure_reasons_for_censured_id(instance.id, [c_instance.id])
|
||||
censures = [c for c in censures if c.reason is not None]
|
||||
|
@ -161,6 +162,7 @@ class Censures(Resource):
|
|||
if len(censures) > 0:
|
||||
c_instance_details["censure_reasons"] = [censure.reason for censure in censures]
|
||||
c_instance_details["censure_evidence"] = [censure.evidence for censure in censures if censure.evidence is not None]
|
||||
c_instance_details["rebuttal"] = [r.rebuttal for r in rebuttals if r.target_id == c_instance.id]
|
||||
instance_details.append(c_instance_details)
|
||||
if self.args.csv:
|
||||
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
|
||||
|
|
|
@ -140,6 +140,7 @@ class Hesitations(Resource):
|
|||
continue
|
||||
instances.append(p_instance)
|
||||
instance_details = []
|
||||
rebuttals = database.get_all_rebuttals_from_source_instance_id(instance.id,[c.id for c in instances])
|
||||
for c_instance in instances:
|
||||
hesitations = database.get_all_hesitation_reasons_for_dubious_id(instance.id, [c_instance.id])
|
||||
hesitations = [c for c in hesitations if c.reason is not None]
|
||||
|
@ -147,6 +148,7 @@ class Hesitations(Resource):
|
|||
if len(hesitations) > 0:
|
||||
c_instance_details["hesitation_reasons"] = [hesitation.reason for hesitation in hesitations]
|
||||
c_instance_details["hesitation_evidence"] = [hesitation.evidence for hesitation in hesitations if hesitation.evidence is not None]
|
||||
c_instance_details["rebuttal"] = [r.rebuttal for r in rebuttals if r.target_id == c_instance.id]
|
||||
instance_details.append(c_instance_details)
|
||||
if self.args.csv:
|
||||
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
from fediseer.apis.v1.base import *
|
||||
from fediseer.classes.instance import Rebuttal
|
||||
from fediseer.utils import sanitize_string
|
||||
from fediseer.classes.reports import Report
|
||||
from fediseer import enums
|
||||
|
||||
class Rebuttals(Resource):
|
||||
decorators = [limiter.limit("45/minute"), limiter.limit("30/minute", key_func = get_request_path)]
|
||||
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")
|
||||
put_parser.add_argument("rebuttal", default=None, type=str, required=True, location="json")
|
||||
|
||||
|
||||
@api.expect(put_parser,models.input_rebuttals_modify, validate=True)
|
||||
@api.marshal_with(models.response_model_simple_response, code=200, description='Rebut Censure or Hesitation against your instance')
|
||||
@api.response(400, 'Bad Request', models.response_model_error)
|
||||
@api.response(401, 'Invalid API Key', models.response_model_error)
|
||||
@api.response(403, 'Access Denied', models.response_model_error)
|
||||
@api.response(404, 'Instance not registered', models.response_model_error)
|
||||
def put(self, domain):
|
||||
'''Rebut a Censure or Hesitation against your instance
|
||||
Use this to provide evidence against, or to initiate discussion
|
||||
'''
|
||||
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 rebut.")
|
||||
if database.instance_has_flag(instance.id,enums.InstanceFlags.RESTRICTED):
|
||||
raise e.Forbidden("You cannot take this action as your instance is restricted")
|
||||
if database.has_too_many_actions_per_min(instance.domain):
|
||||
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
|
||||
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)
|
||||
if not target_instance:
|
||||
raise e.NotFound(f"Instance {domain} is not registered on the fediseer.")
|
||||
if database.get_rebuttal(target_instance.id,instance.id):
|
||||
return {"message":'OK'}, 200
|
||||
censure = database.get_censure(instance.id, target_instance.id)
|
||||
if not censure or target_instance.visibility_censures != enums.ListVisibility.OPEN:
|
||||
hesitation = database.get_hesitation(instance.id, target_instance.id)
|
||||
if not hesitation or target_instance.visibility_hesitations != enums.ListVisibility.OPEN:
|
||||
raise e.BadRequest(f"Either no censure or hesitation from {domain} found towards {instance.domain}, or they are not openly visible.")
|
||||
rebuttal_value = self.args.rebuttal
|
||||
if rebuttal_value is not None:
|
||||
rebuttal_value = sanitize_string(rebuttal_value)
|
||||
new_rebuttal = Rebuttal(
|
||||
source_id=instance.id,
|
||||
target_id=target_instance.id,
|
||||
rebuttal=rebuttal_value,
|
||||
)
|
||||
db.session.add(new_rebuttal)
|
||||
target_domain = target_instance.domain
|
||||
new_report = Report(
|
||||
source_domain=instance.domain,
|
||||
target_domain=target_domain,
|
||||
report_type=enums.ReportType.REBUTTAL,
|
||||
report_activity=enums.ReportActivity.ADDED,
|
||||
)
|
||||
db.session.add(new_report)
|
||||
db.session.commit()
|
||||
logger.info(f"{instance.domain} Rebutted {domain}")
|
||||
return {"message":'Changed'}, 200
|
||||
|
||||
|
||||
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
|
||||
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("rebuttal", default=None, type=str, required=False, location="json")
|
||||
|
||||
|
||||
@api.expect(patch_parser,models.input_rebuttals_modify, validate=True)
|
||||
@api.marshal_with(models.response_model_simple_response, code=200, description='Modify Rebuttal')
|
||||
@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 patch(self, domain):
|
||||
'''Modify a Rebuttal
|
||||
'''
|
||||
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 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 database.has_too_many_actions_per_min(instance.domain):
|
||||
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
|
||||
target_instance = database.find_instance_by_domain(domain=domain)
|
||||
if not target_instance:
|
||||
raise e.BadRequest("Instance from which to modify censure not found")
|
||||
rebuttal = database.get_rebuttal(target_instance.id,instance.id)
|
||||
if not rebuttal:
|
||||
raise e.BadRequest(f"No Rebuttal found for {domain} from {instance.domain}")
|
||||
changed = False
|
||||
rebuttal_value = self.args.rebuttal
|
||||
if rebuttal_value is not None:
|
||||
rebuttal_value = sanitize_string(rebuttal_value)
|
||||
if rebuttal.rebuttal != rebuttal_value:
|
||||
rebuttal.rebuttal = rebuttal_value
|
||||
changed = True
|
||||
if changed is False:
|
||||
return {"message":'OK'}, 200
|
||||
target_domain = target_instance.domain
|
||||
if instance.visibility_censures != enums.ListVisibility.OPEN:
|
||||
target_domain = '[REDACTED]'
|
||||
new_report = Report(
|
||||
source_domain=instance.domain,
|
||||
target_domain=target_domain,
|
||||
report_type=enums.ReportType.REBUTTAL,
|
||||
report_activity=enums.ReportActivity.MODIFIED,
|
||||
)
|
||||
db.session.add(new_report)
|
||||
db.session.commit()
|
||||
logger.info(f"{instance.domain} modified rebuttal about {domain}")
|
||||
return {"message":'Changed'}, 200
|
||||
|
||||
|
||||
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
|
||||
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='Delete Rebuttal')
|
||||
@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):
|
||||
'''Delete a rebuttal
|
||||
'''
|
||||
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")
|
||||
rebuttal = database.get_rebuttal(target_instance.id,instance.id)
|
||||
if not rebuttal:
|
||||
return {"message":'OK'}, 200
|
||||
db.session.delete(rebuttal)
|
||||
target_domain = target_instance.domain
|
||||
new_report = Report(
|
||||
source_domain=instance.domain,
|
||||
target_domain=target_domain,
|
||||
report_type=enums.ReportType.REBUTTAL,
|
||||
report_activity=enums.ReportActivity.DELETED,
|
||||
)
|
||||
db.session.add(new_report)
|
||||
db.session.commit()
|
||||
logger.info(f"{instance.domain} delete rebuttal about {domain}")
|
||||
return {"message":'Changed'}, 200
|
|
@ -72,6 +72,17 @@ class Hesitation(db.Model):
|
|||
dubious_instance = db.relationship("Instance", back_populates="hesitations_received", foreign_keys=[dubious_id])
|
||||
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
class Rebuttal(db.Model):
|
||||
__tablename__ = "rebuttals"
|
||||
__table_args__ = (UniqueConstraint('source_id', 'target_id', name='rebuttal_source_id_target_id'),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
rebuttal = db.Column(db.Text, unique=False, nullable=False, index=False)
|
||||
source_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
source_instance = db.relationship("Instance", back_populates="rebuttals_given", foreign_keys=[source_id])
|
||||
target_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
target_instance = db.relationship("Instance", back_populates="rebuttals_received", foreign_keys=[target_id])
|
||||
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
class Solicitation(db.Model):
|
||||
__tablename__ = "solicitations"
|
||||
__table_args__ = (UniqueConstraint('source_id', 'target_id', name='solicitations_source_id_target_id'),)
|
||||
|
@ -134,6 +145,8 @@ class Instance(db.Model):
|
|||
censures_received = db.relationship("Censure", back_populates="censured_instance", cascade="all, delete-orphan", foreign_keys=[Censure.censured_id])
|
||||
hesitations_given = db.relationship("Hesitation", back_populates="hesitating_instance", cascade="all, delete-orphan", foreign_keys=[Hesitation.hesitant_id])
|
||||
hesitations_received = db.relationship("Hesitation", back_populates="dubious_instance", cascade="all, delete-orphan", foreign_keys=[Hesitation.dubious_id])
|
||||
rebuttals_given = db.relationship("Rebuttal", back_populates="source_instance", cascade="all, delete-orphan", foreign_keys=[Rebuttal.source_id])
|
||||
rebuttals_received = db.relationship("Rebuttal", back_populates="target_instance", cascade="all, delete-orphan", foreign_keys=[Rebuttal.target_id])
|
||||
solicitations_requested = db.relationship("Solicitation", back_populates="source_instance", cascade="all, delete-orphan", foreign_keys=[Solicitation.source_id])
|
||||
solicitations_received = db.relationship("Solicitation", back_populates="target_instance", cascade="all, delete-orphan", foreign_keys=[Solicitation.target_id])
|
||||
guarantees = db.relationship("Guarantee", back_populates="guarantor_instance", cascade="all, delete-orphan", foreign_keys=[Guarantee.guarantor_id])
|
||||
|
|
|
@ -4,7 +4,7 @@ from sqlalchemy import func, or_, and_, not_, Boolean
|
|||
from fediseer.flask import db
|
||||
from fediseer.utils import hash_api_key
|
||||
from sqlalchemy.orm import joinedload
|
||||
from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure, Hesitation, Solicitation, InstanceFlag, InstanceTag
|
||||
from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure, Hesitation, Solicitation, InstanceFlag, InstanceTag, Rebuttal
|
||||
from fediseer.classes.user import Claim, User
|
||||
from fediseer.classes.reports import Report
|
||||
from fediseer import enums
|
||||
|
@ -93,10 +93,8 @@ def get_all_approving_instances_by_endorsed_id(endorsed_id):
|
|||
|
||||
def get_all_endorsement_reasons_for_endorsed_id(endorsed_id, approving_ids):
|
||||
query = Endorsement.query.filter(
|
||||
and_(
|
||||
Endorsement.endorsed_id == endorsed_id,
|
||||
Endorsement.approving_id.in_(approving_ids),
|
||||
)
|
||||
).with_entities(
|
||||
Endorsement.reason,
|
||||
)
|
||||
|
@ -146,10 +144,8 @@ def get_all_censuring_instances_by_censured_id(censured_id):
|
|||
|
||||
def get_all_censure_reasons_for_censured_id(censured_id, censuring_ids):
|
||||
query = Censure.query.filter(
|
||||
and_(
|
||||
Censure.censured_id == censured_id,
|
||||
Censure.censuring_id.in_(censuring_ids),
|
||||
)
|
||||
).with_entities(
|
||||
Censure.reason,
|
||||
Censure.evidence,
|
||||
|
@ -200,17 +196,31 @@ def get_all_hesitant_instances_by_dubious_id(dubious_id):
|
|||
|
||||
def get_all_hesitation_reasons_for_dubious_id(dubious_id, hesitant_ids):
|
||||
query = Hesitation.query.filter(
|
||||
and_(
|
||||
Hesitation.dubious_id == dubious_id,
|
||||
Hesitation.hesitant_id.in_(hesitant_ids),
|
||||
)
|
||||
).with_entities(
|
||||
Hesitation.reason,
|
||||
Hesitation.evidence,
|
||||
)
|
||||
return query.all()
|
||||
|
||||
def get_rebuttal(target_instance_id, source_instance_id):
|
||||
query = Rebuttal.query.filter_by(
|
||||
target_id=target_instance_id,
|
||||
source_id=source_instance_id,
|
||||
)
|
||||
return query.first()
|
||||
|
||||
def get_all_rebuttals_from_source_instance_id(source_instance_id, target_ids = None):
|
||||
query = Rebuttal.query.filter(
|
||||
Rebuttal.source_id == source_instance_id,
|
||||
).with_entities(
|
||||
Rebuttal.rebuttal,
|
||||
Rebuttal.target_id,
|
||||
)
|
||||
if target_ids is not None:
|
||||
query = query.filter(Rebuttal.target_id.in_(target_ids))
|
||||
return query.all()
|
||||
|
||||
def get_all_guaranteed_instances_by_guarantor_id(guarantor_id):
|
||||
query = db.session.query(
|
||||
|
|
|
@ -8,6 +8,7 @@ class ReportType(enum.Enum):
|
|||
CLAIM = 4
|
||||
SOLICITATION = 5
|
||||
FLAG = 6
|
||||
REBUTTAL = 7
|
||||
|
||||
class ReportActivity(enum.Enum):
|
||||
ADDED = 0
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TYPE reporttype ADD VALUE 'REBUTTAL';
|
Loading…
Reference in New Issue