feat: Added solicitations (#23)

* feat: Added solicitations

* feat: working solicitations
pull/26/head
Divided by Zer0 2023-09-16 13:44:07 +02:00 committed by GitHub
parent 2a50a686df
commit f8b4eeee51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 250 additions and 26 deletions

View File

@ -1,5 +1,10 @@
# Changelog
# 0.15.0
* Added solicitation. Now you can see which instances are requesting guarantees
* Orphaned instances will automatically receive an open solicitation
# 0.14.1
* Fixed a bug with returning the reset API key on response

View File

@ -85,7 +85,7 @@ class Models:
})
self.input_instance_claim = api.model('ClaimInstanceInput', {
'admin': fields.String(required=True, min_length=1, description="The username of the admin who wants to register this domain", example="admin"),
'guarantor': fields.String(required=False, description="(Optional) The domain of the guaranteeing instance. They will receive a PM to validate you", example="admin"),
'guarantor': fields.String(required=False, description="(Optional) The domain of the guaranteeing instance. They will receive a PM to validate you", example="lemmy.dbzer0.com"),
'pm_proxy': fields.String(required=False, enum=[e.name for e in enums.PMProxy], description="(Optional) If you do receive the PM from @fediseer@fediseer.com, set this to 'MASTODON' to make the Fediseer PM your your API key via @fediseer@botsin.space. For this to work, ensure that botsin.space is not blocked in your instance and optimally follow @fediseer@botsin.space as well. If set, this will be used permanently for communication to your instance."),
})
self.input_api_key_reset = api.model('ApiKeyResetInput', {
@ -101,4 +101,17 @@ class Models:
'report_type': fields.String(description="The type of report activity", enum=[e.name for e in enums.ReportType]),
'report_activity': fields.String(description="The activity reported", enum=[e.name for e in enums.ReportActivity]),
'created': fields.DateTime(description="The date this record was added"),
})
})
self.input_solicit = api.model('SolicitInput', {
'guarantor': fields.String(required=False, description="The domain of the instance to solicit for a guarantee. They will receive a PM to guarantee for you", example="lemmy.dbzer0.com", min_length=1, max_length=255),
'comment': fields.String(required=False, description="You can provide some info about your instance here.", example="Me No Spam!", min_length=1, max_length=1000),
})
self.response_model_instances_soliciting = api.inherit('SolicitingInstanceDetails', self.response_model_instances, {
'comment': fields.String(description="The optional comment explaining why this instance deserves a guarantee"),
})
self.response_model_model_Solicitation_get = api.model('SolicitedInstances', {
'instances': fields.List(fields.Nested(self.response_model_instances_soliciting)),
'domains': fields.List(fields.String(description="The instance domains as a list.")),
'csv': fields.String(description="The instance domains as a csv."),
})

View File

@ -1,5 +1,6 @@
import fediseer.apis.v1.base as base
import fediseer.apis.v1.whitelist as whitelist
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
@ -15,6 +16,7 @@ api.add_resource(find.FindInstance, "/find_instance")
api.add_resource(activitypub.User, "/user/<string:username>")
api.add_resource(activitypub.Inbox, "/inbox/<string:username>")
api.add_resource(whitelist.Whitelist, "/whitelist")
api.add_resource(solicitations.Solicitations, "/solicitations")
api.add_resource(whitelist.WhitelistDomain, "/whitelist/<string:domain>")
api.add_resource(endorsements.Endorsements, "/endorsements/<string:domain>")
api.add_resource(endorsements.Approvals, "/approvals/<string:domains_csv>")

View File

@ -1,5 +1,5 @@
from fediseer.apis.v1.base import *
from fediseer.classes.instance import Guarantee, Endorsement, RejectionRecord
from fediseer.classes.instance import Guarantee, RejectionRecord, Solicitation
from fediseer.classes.reports import Report
from fediseer import enums
@ -99,12 +99,7 @@ class Guarantees(Resource):
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)
database.delete_all_solicitation_by_source(target_instance.id)
new_report = Report(
source_domain=instance.domain,
target_domain=target_instance.domain,
@ -172,6 +167,22 @@ class Guarantees(Resource):
endorsement = database.get_endorsement(target_instance.id,instance.id)
if endorsement:
db.session.delete(endorsement)
# Orphaned instances are automatically put into the solicitation list
new_solicitation = Solicitation(
comment="Orphaned instance!",
source_id=target_instance.id,
target_id=None,
created=guarantee.created,
)
db.session.add(new_solicitation)
solicitation_report = Report(
source_domain=instance.domain,
target_domain=instance.domain,
report_type=enums.ReportType.SOLICITATION,
report_activity=enums.ReportActivity.ADDED,
)
db.session.add(solicitation_report)
db.session.delete(guarantee)
rejection_record = database.get_rejection_record(instance.id,target_instance.id)
if rejection_record:
@ -188,6 +199,7 @@ class Guarantees(Resource):
report_type=enums.ReportType.GUARANTEE,
report_activity=enums.ReportActivity.DELETED,
)
db.session.add(new_report)
db.session.commit()
try:

View File

@ -0,0 +1,95 @@
from fediseer.apis.v1.base import *
from fediseer.messaging import activitypub_pm
from fediseer import enums
from fediseer.classes.instance import Solicitation
from fediseer.classes.reports import Report
class Solicitations(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, query_string=True)
@cache.cached(timeout=10)
@api.marshal_with(models.response_model_model_Solicitation_get, code=200, description='Soliciting Instances', skip_none=True)
def get(self):
'''A List with all the currently open solicitations for guarantees.
'''
self.args = self.get_parser.parse_args()
instance_details = []
for instance in database.get_all_solicitations():
instance_detail = instance.get_details()
instance_detail["comment"] = database.find_latest_solicitation_by_source(instance.id).comment
instance_details.append(instance_detail)
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
post_parser = reqparse.RequestParser()
post_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
post_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
post_parser.add_argument("guarantor", required=False, type=str, help="(Optional) The domain of a guaranteeing instance. They will receive a PM to validate you", location="json")
post_parser.add_argument("comment", required=False, type=str, location="json")
@api.expect(post_parser,models.input_solicit, validate=True)
@api.marshal_with(models.response_model_simple_response, code=200, description='Instances')
@api.response(400, 'Bad Request', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Recent solicitation exists', models.response_model_error)
@api.response(404, 'Instance not claimed', models.response_model_error)
def post(self):
'''Solicit a guarantee
This will add your instance to the list of requested guarantees,
Other guaranteeed instances can review your application and decide to guarantee for you.
You can optionally provide the domain of an instance to receive a PM requesting for your guarantee
'''
self.args = self.post_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 claim it?")
if instance.is_guaranteed():
raise e.BadRequest(f"Your instance is already guaranteed by {instance.get_guarantor().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 Fediseer yet!")
existing_solicitation = database.find_solicitation_by_target(instance.id,guarantor_instance.id)
if existing_solicitation:
raise e.Forbidden(f"You have already solicited this instance for a guarantee. Please solicit a different guarantor instead.")
else:
existing_solicitation = database.find_solicitation_by_target(instance.id,None)
if existing_solicitation:
raise e.Forbidden(f"You have already solicited an open-ended guarantee. Please try to solicit from a specific instance next.")
if database.has_recent_solicitations(instance.id):
raise e.Forbidden(f"You can only solicit one guarantee per day.")
new_solicitation = Solicitation(
comment=self.args.comment,
source_id=instance.id,
target_id=guarantor_instance.id if guarantor_instance else None,
)
db.session.add(new_solicitation)
new_report = Report(
source_domain=instance.domain,
target_domain=guarantor_instance.domain if guarantor_instance else instance.domain,
report_type=enums.ReportType.SOLICITATION,
report_activity=enums.ReportActivity.ADDED,
)
db.session.add(new_report)
db.session.commit()
if guarantor_instance:
try:
activitypub_pm.pm_admins(
message=f"New instance {instance.domain} was just registered with the Fediseer and have solicited [your guarantee](https://gui.fediseer.com/guarantees/guarantee)!",
domain=guarantor_instance.domain,
software=guarantor_instance.software,
instance=guarantor_instance,
)
except:
pass
return {"message":'Changed'}, 200

View File

@ -2,6 +2,8 @@ from fediseer.apis.v1.base import *
from fediseer.messaging import activitypub_pm
from fediseer.classes.user import User, Claim
from fediseer import enums
from fediseer.classes.instance import Solicitation
from fediseer.classes.reports import Report
class Whitelist(Resource):
get_parser = reqparse.RequestParser()
@ -9,7 +11,7 @@ class Whitelist(Resource):
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=str, help="Set to true to return just the domains as a list. Mutually exclusive with csv", 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)
@ -49,7 +51,7 @@ class WhitelistDomain(Resource):
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=True, 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="(Optional) The domain of the guaranteeing instance. They will receive a PM to validate you", location="json")
put_parser.add_argument("guarantor", required=False, type=str, help="(Optional) The domain of another guaranteed instance. They will receive a PM to validate you and you will be added to the solicitations list.", location="json")
put_parser.add_argument("pm_proxy", required=False, type=str, help="(Optional) If you do receive the PM from @fediseer@fediseer.com, set this to true to make the Fediseer PM your your API key via @fediseer@botsin.space. For this to work, ensure that botsin.space is not blocked in your instance and optimally follow @fediseer@botsin.space as well. If set, this will be used permanently for communication to your instance.", location="json")
@ -102,11 +104,31 @@ class WhitelistDomain(Resource):
instance_id = instance.id,
)
db.session.add(new_claim)
new_report = Report(
source_domain=instance.domain,
target_domain=instance.domain,
report_type=enums.ReportType.CLAIM,
report_activity=enums.ReportActivity.ADDED,
)
db.session.add(new_report)
db.session.commit()
if guarantor_instance:
if guarantor_instance and not instance.is_guaranteed():
new_solicitation = Solicitation(
source_id=instance.id,
target_id=guarantor_instance.id,
)
db.session.add(new_solicitation)
solicitation_report = Report(
source_domain=instance.domain,
target_domain=guarantor_instance.domain,
report_type=enums.ReportType.SOLICITATION,
report_activity=enums.ReportActivity.ADDED,
)
db.session.add(solicitation_report)
db.session.commit()
try:
activitypub_pm.pm_admins(
message=f"New instance {domain} was just registered with the Fediseer and have asked you to guarantee for them!",
message=f"New instance {instance.domain} was just registered with the Fediseer and have solicited [your guarantee](https://gui.fediseer.com/guarantees/guarantee)!",
domain=guarantor_instance.domain,
software=guarantor_instance.software,
instance=guarantor_instance,

View File

@ -18,9 +18,9 @@ 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_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
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_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
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)
@ -32,9 +32,9 @@ class RejectionRecord(db.Model):
class Guarantee(db.Model):
__tablename__ = "guarantees"
id = db.Column(db.Integer, primary_key=True)
guarantor_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
guarantor_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
guarantor_instance = db.relationship("Instance", back_populates="guarantees", foreign_keys=[guarantor_id])
guaranteed_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), unique=True, nullable=False)
guaranteed_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), unique=True, nullable=False, index=True)
guaranteed_instance = db.relationship("Instance", back_populates="guarantors", foreign_keys=[guaranteed_id])
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
@ -44,9 +44,9 @@ class Endorsement(db.Model):
__table_args__ = (UniqueConstraint('approving_id', 'endorsed_id', name='endoresements_approving_id_endorsed_id'),)
id = db.Column(db.Integer, primary_key=True)
reason = db.Column(db.String(255), unique=False, nullable=True, index=False)
approving_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
approving_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
approving_instance = db.relationship("Instance", back_populates="approvals", foreign_keys=[approving_id])
endorsed_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
endorsed_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
endorsed_instance = db.relationship("Instance", back_populates="endorsements", foreign_keys=[endorsed_id])
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
@ -56,9 +56,9 @@ class Censure(db.Model):
id = db.Column(db.Integer, primary_key=True)
reason = db.Column(db.String(255), unique=False, nullable=True, index=False)
evidence = db.Column(db.Text, unique=False, nullable=True, index=False)
censuring_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
censuring_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
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_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
censured_instance = db.relationship("Instance", back_populates="censures_received", foreign_keys=[censured_id])
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
@ -68,12 +68,22 @@ class Hesitation(db.Model):
id = db.Column(db.Integer, primary_key=True)
reason = db.Column(db.String(255), unique=False, nullable=True, index=False)
evidence = db.Column(db.Text, unique=False, nullable=True, index=False)
hesitant_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
hesitant_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
hesitating_instance = db.relationship("Instance", back_populates="hesitations_given", foreign_keys=[hesitant_id])
dubious_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False)
dubious_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
dubious_instance = db.relationship("Instance", back_populates="hesitations_received", foreign_keys=[dubious_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'),)
id = db.Column(db.Integer, primary_key=True)
comment = db.Column(db.Text, unique=False, nullable=True, 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="solicitations_requested", foreign_keys=[source_id])
target_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=True, index=True)
target_instance = db.relationship("Instance", back_populates="solicitations_received", foreign_keys=[target_id])
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
class Instance(db.Model):
__tablename__ = "instances"
@ -98,6 +108,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])
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])
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])
@ -130,6 +142,9 @@ class Instance(db.Model):
return None
return self.guarantors[0]
def is_guaranteed(self):
return len(self.guarantors) > 0
def get_guarantor(self):
guarantee = self.get_guarantee()
if not guarantee:
@ -147,4 +162,4 @@ class Instance(db.Model):
def unset_as_orphan(self):
self.oprhan_since = None
db.session.commit()
db.session.commit()

View File

@ -1,4 +1,4 @@
FEDISEER_VERSION = "0.14.1"
FEDISEER_VERSION = "0.15.0"
SUPPORTED_SOFTWARE = {
"lemmy",
"mastodon",

View File

@ -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, Censure, Hesitation
from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure, Hesitation, Solicitation
from fediseer.classes.user import Claim, User
from fediseer.classes.reports import Report
from fediseer import enums
@ -391,3 +391,59 @@ def get_reports(
if page < 0:
page = 0
return query.order_by(Report.created.desc()).offset(10 * page).limit(10).all()
def get_all_solicitations():
# Subquery to find the minimum created date for each source_instance
subq = db.session.query(
Solicitation.source_id,
func.min(Solicitation.created).label('oldest_solicitation_date')
).group_by(
Solicitation.source_id
).subquery()
# Query to retrieve instances with at least one solicitation
query = db.session.query(
Instance,
).join(
subq,
Instance.id == subq.c.source_id
).order_by(
subq.c.oldest_solicitation_date
)
return query.all()
def find_solicitation_by_target(source_id, target_id):
query = db.session.query(
Solicitation
).filter(
Solicitation.source_id == source_id,
Solicitation.target_id == target_id,
)
return query.first()
def delete_all_solicitation_by_source(source_id):
query = db.session.query(
Solicitation
).filter(
Solicitation.source_id == source_id,
)
query.delete()
def has_recent_solicitations(source_id):
query = db.session.query(
Solicitation
).filter(
Solicitation.source_id == source_id,
Solicitation.created > datetime.utcnow() - timedelta(hours=24),
)
return query.count() > 0
def find_latest_solicitation_by_source(source_id):
query = db.session.query(
Solicitation
).filter(
Solicitation.source_id == source_id,
)
return query.order_by(Solicitation.created.desc()).first()

View File

@ -5,6 +5,8 @@ class ReportType(enum.Enum):
ENDORSEMENT = 1
CENSURE = 2
HESITATION = 3
CLAIM = 4
SOLICITATION = 5
class ReportActivity(enum.Enum):
ADDED = 0

View File

@ -0,0 +1,2 @@
ALTER TYPE reporttype ADD VALUE 'CLAIM';
ALTER TYPE reporttype ADD VALUE 'SOLICITATION';