diff --git a/CHANGELOG.md b/CHANGELOG.md
index f066797a..e6595031 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+# 4.36.0
+
+* Added Document endpoints
+
# 4.35.1
* Added allow_sdxl_controlnet worker key
diff --git a/horde/apis/models/v2.py b/horde/apis/models/v2.py
index 98e071c4..099f84ff 100644
--- a/horde/apis/models/v2.py
+++ b/horde/apis/models/v2.py
@@ -1507,3 +1507,16 @@ def __init__(self, api):
"reference": fields.String(description="The reference which points how and where this text should be used.", min_length=3),
},
)
+ self.response_model_doc_terms = api.model(
+ "HordeDocument",
+ {
+ "html": fields.String(
+ required=False,
+ description="The document in html format.",
+ ),
+ "markdown": fields.String(
+ required=False,
+ description="The document in markdown format.",
+ ),
+ },
+ )
diff --git a/horde/apis/v2/__init__.py b/horde/apis/v2/__init__.py
index b3fcaffc..0ba7ddd7 100644
--- a/horde/apis/v2/__init__.py
+++ b/horde/apis/v2/__init__.py
@@ -45,3 +45,6 @@
api.add_resource(stable.ImageHordeStatsModels, "/stats/img/models")
api.add_resource(kobold.TextHordeStatsTotals, "/stats/text/totals")
api.add_resource(kobold.TextHordeStatsModels, "/stats/text/models")
+api.add_resource(base.DocsTerms, "/documents/terms")
+api.add_resource(base.DocsPrivacy, "/documents/privacy")
+api.add_resource(base.DocsSponsors, "/documents/sponsors")
diff --git a/horde/apis/v2/base.py b/horde/apis/v2/base.py
index 028385a7..48e7b9eb 100644
--- a/horde/apis/v2/base.py
+++ b/horde/apis/v2/base.py
@@ -4,9 +4,10 @@
from datetime import datetime, timedelta
import regex as re
-from flask import request
+from flask import render_template, request
from flask_restx import Namespace, Resource, reqparse
from flask_restx.reqparse import ParseResult
+from markdownify import markdownify
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError, InvalidRequestError
@@ -35,7 +36,7 @@
from horde.r2 import upload_prompt
from horde.suspicions import Suspicions
from horde.utils import hash_api_key, hash_dictionary, is_profane, sanitize_string
-from horde.vars import horde_title
+from horde.vars import horde_contact_email, horde_title, horde_url
# Not used yet
authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "apikey"}}
@@ -2914,3 +2915,136 @@ def delete(self, sharedkey_id=""):
db.session.delete(sharedkey)
db.session.commit()
return {"message": "OK"}, 200
+
+
+class DocsTerms(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(
+ "format",
+ required=False,
+ default="html",
+ type=str,
+ help="html or markdown",
+ location="args",
+ )
+
+ # @cache.cached(timeout=3600, query_string=True)
+ @api.response(400, "Validation Error", models.response_model_error)
+ @api.expect(get_parser)
+ @api.marshal_with(
+ models.response_model_doc_terms,
+ code=200,
+ description=f"{horde_title} documentation",
+ skip_none=True,
+ )
+ def get(self):
+ """Terms and Conditions"""
+ self.args = self.get_parser.parse_args()
+ if self.args.format not in ["html", "markdown"]:
+ raise e.BadRequest("'format' needs to be one of ['html', 'markdown']")
+ html_template = render_template(
+ os.getenv("HORDE_HTML_TERMS", "terms_of_service.html"),
+ horde_title=horde_title,
+ horde_url=horde_url,
+ horde_contact_email=horde_contact_email,
+ )
+ if self.args.format == "markdown":
+ return {"markdown": markdownify(html_template).strip("\n")}, 200
+ return {"html": html_template}, 200
+
+
+class DocsPrivacy(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(
+ "format",
+ required=False,
+ default="html",
+ type=str,
+ help="html or markdown",
+ location="args",
+ )
+
+ # @cache.cached(timeout=3600, query_string=True)
+ @api.response(400, "Validation Error", models.response_model_error)
+ @api.expect(get_parser)
+ @api.marshal_with(
+ models.response_model_doc_terms,
+ code=200,
+ description=f"{horde_title} documentation",
+ skip_none=True,
+ )
+ def get(self):
+ """Privacy Policy"""
+ self.args = self.get_parser.parse_args()
+ if self.args.format not in ["html", "markdown"]:
+ raise e.BadRequest("'format' needs to be one of ['html', 'markdown']")
+ html_template = render_template(
+ os.getenv("HORDE_HTML_PRIVACY", "privacy_policy.html"),
+ horde_title=horde_title,
+ horde_url=horde_url,
+ horde_contact_email=horde_contact_email,
+ )
+ if self.args.format == "markdown":
+ return {"markdown": markdownify(html_template).strip("\n")}, 200
+ return {"html": html_template}, 200
+
+
+class DocsSponsors(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(
+ "format",
+ required=False,
+ default="html",
+ type=str,
+ help="html or markdown",
+ location="args",
+ )
+
+ # @cache.cached(timeout=3600, query_string=True)
+ @api.response(400, "Validation Error", models.response_model_error)
+ @api.expect(get_parser)
+ @api.marshal_with(
+ models.response_model_doc_terms,
+ code=200,
+ description=f"{horde_title} documentation",
+ skip_none=True,
+ )
+ def get(self):
+ """Sponsors"""
+ self.args = self.get_parser.parse_args()
+ if self.args.format not in ["html", "markdown"]:
+ raise e.BadRequest("'format' needs to be one of ['html', 'markdown']")
+ all_patrons = ", ".join(patrons.get_names(min_entitlement=3, max_entitlement=99))
+ html_template = render_template(
+ "sponsors.html",
+ page_title="Sponsors",
+ all_patrons=all_patrons,
+ all_sponsors=patrons.get_sponsors(),
+ )
+ if self.args.format == "markdown":
+ return {"markdown": markdownify(html_template).strip("\n")}, 200
+ return {"html": html_template}, 200
diff --git a/horde/consts.py b/horde/consts.py
index 54a985da..d56e4456 100644
--- a/horde/consts.py
+++ b/horde/consts.py
@@ -1,4 +1,4 @@
-HORDE_VERSION = "4.35.1"
+HORDE_VERSION = "4.36.0"
WHITELISTED_SERVICE_IPS = {
"212.227.227.178", # Turing Bot
diff --git a/horde/routes.py b/horde/routes.py
index ca687924..1a29ac7e 100644
--- a/horde/routes.py
+++ b/horde/routes.py
@@ -140,7 +140,8 @@ def index():
def patrons_route():
all_patrons = ", ".join(patrons.get_names(min_entitlement=3, max_entitlement=99))
return render_template(
- "sponsors.html",
+ "document.html",
+ doc="sponsors.html",
page_title="Sponsors",
all_patrons=all_patrons,
all_sponsors=patrons.get_sponsors(),
@@ -364,7 +365,8 @@ def finish_dance():
@HORDE.route("/privacy")
def privacy():
return render_template(
- os.getenv("HORDE_HTML_PRIVACY", "privacy_policy.html"),
+ "document.html",
+ doc=os.getenv("HORDE_HTML_TERMS", "privacy_policy.html"),
horde_title=horde_title,
horde_url=horde_url,
horde_contact_email=horde_contact_email,
@@ -374,7 +376,8 @@ def privacy():
@HORDE.route("/terms")
def terms():
return render_template(
- os.getenv("HORDE_HTML_TERMS", "terms_of_service.html"),
+ "document.html",
+ doc=os.getenv("HORDE_HTML_TERMS", "terms_of_service.html"),
horde_title=horde_title,
horde_url=horde_url,
horde_contact_email=horde_contact_email,
diff --git a/horde/templates/document.html b/horde/templates/document.html
new file mode 100644
index 00000000..0a18fab9
--- /dev/null
+++ b/horde/templates/document.html
@@ -0,0 +1,7 @@
+{% extends "master.html" %}
+{% block content %}
+{% include doc %}
+
+
+
Last updated: 18 Mar, 2024
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
@@ -153,5 +151,3 @@Last updated: 07 January, 2023
Please read these terms and conditions carefully before using Our Service.
@@ -122,6 +120,4 @@If you have any questions about these Terms and Conditions, You can contact us: