Add Minetest-optimised package endpoint

This commit is contained in:
rubenwardy 2024-04-01 17:32:12 +01:00
parent 0f5a97b539
commit 1be4155ab0
4 changed files with 118 additions and 11 deletions

@ -35,7 +35,7 @@ from app.models import Tag, PackageState, PackageType, Package, db, PackageRelea
PackageAlias, Language PackageAlias, Language
from app.querybuilder import QueryBuilder from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date
from app.utils.minetest_hypertext import html_to_minetest from app.utils.minetest_hypertext import html_to_minetest, package_info_as_hypertext
from . import bp from . import bp
from .auth import is_api_authd from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \ from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
@ -110,12 +110,42 @@ def package_view(package):
lang = request.accept_languages.best_match(allowed_languages) lang = request.accept_languages.best_match(allowed_languages)
data = package.as_dict(current_app.config["BASE_URL"], lang=lang) data = package.as_dict(current_app.config["BASE_URL"], lang=lang)
if "formspec_version" in request.args: resp = jsonify(data)
formspec_version = request.args["formspec_version"] resp.vary = "Accept-Language"
return resp
@bp.route("/api/packages/<author>/<name>/for-client/")
@is_package_page
@cors_allowed
def package_view_client(package: Package):
protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
version = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
else:
version = None
allowed_languages = set([x[0] for x in db.session.query(Language.id).all()])
lang = request.accept_languages.best_match(allowed_languages)
data = package.as_dict(current_app.config["BASE_URL"], version, lang=lang)
formspec_version = get_int_or_abort(request.args["formspec_version"])
include_images = is_yes(request.args.get("include_images", "true")) include_images = is_yes(request.args.get("include_images", "true"))
html = render_markdown(data["long_description"]) html = render_markdown(data["long_description"])
data["long_description"] = html_to_minetest(html, formspec_version, include_images) data["long_description"] = html_to_minetest(html, formspec_version, include_images)
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
data["download_size"] = package.get_download_release(version).file_size
data["reviews"] = {
"positive": package.reviews.filter(PackageReview.rating > 3).count(),
"neutral": package.reviews.filter(PackageReview.rating == 3).count(),
"negative": package.reviews.filter(PackageReview.rating < 3).count(),
}
resp = jsonify(data) resp = jsonify(data)
resp.vary = "Accept-Language" resp.vary = "Accept-Language"
return resp return resp

@ -78,9 +78,6 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* GET `/api/packages/` (List) * GET `/api/packages/` (List)
* See [Package Queries](#package-queries) * See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/` (Read) * GET `/api/packages/<username>/<name>/` (Read)
* Query arguments
* `formspec_version`: Optional. If present, `long_description` is returned a hypertext (see /hypertext/ below).
* `include_images`: Optional, defaults to true. Only used if `formspec_version` is provided.
* PUT `/api/packages/<author>/<name>/` (Update) * PUT `/api/packages/<author>/<name>/` (Update)
* Requires authentication. * Requires authentication.
* JSON dictionary with any of these keys (all are optional, null to delete Nullables): * JSON dictionary with any of these keys (all are optional, null to delete Nullables):
@ -103,6 +100,15 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* `donate_url`: URL to a donation page. * `donate_url`: URL to a donation page.
* `translation_url`: URL to send users interested in translating your package. * `translation_url`: URL to send users interested in translating your package.
* `game_support`: Array of game support information objects. Not currently documented, as subject to change. * `game_support`: Array of game support information objects. Not currently documented, as subject to change.
* GET `/api/packages/<username>/<name>/for-client/`
* Similar to the read endpoint, but optimised for the Minetest client
* `long_description` is given as a hypertext object, see `/hypertext/` below.
* `info_hypertext` is the info sidebar as a hypertext object.
* Query arguments
* `formspec_version`: Required. See /hypertext/ below.
* `include_images`: Optional, defaults to true.
* `protocol_version`: Optional, used to get the correct release.
* `engine_version`: Optional, used to get the correct release. Ex: `5.3.0`.
* GET `/api/packages/<author>/<name>/hypertext/` * GET `/api/packages/<author>/<name>/hypertext/`
* Converts the long description to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language) * Converts the long description to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element. to be used in a `hypertext` formspec element.

@ -671,7 +671,7 @@ class Package(db.Model):
return url_for("packages.move_to_state", return url_for("packages.move_to_state",
author=self.author.username, name=self.name, state=state.name.lower()) author=self.author.username, name=self.name, state=state.name.lower())
def get_download_release(self, version=None): def get_download_release(self, version=None) -> typing.Optional["PackageRelease"]:
for rel in self.releases: for rel in self.releases:
if rel.approved and (version is None or if rel.approved and (version is None or
((rel.min_rel is None or rel.min_rel_id <= version.id) and ((rel.min_rel is None or rel.min_rel_id <= version.id) and

@ -18,6 +18,10 @@ from html.parser import HTMLParser
import re import re
import sys import sys
from flask_babel import gettext
from app.models import Package, PackageType
def normalize_whitespace(x): def normalize_whitespace(x):
return re.sub(r"\s+", " ", x) return re.sub(r"\s+", " ", x)
@ -188,7 +192,7 @@ class MinetestHTMLParser(HTMLParser):
self.current_line += f"&{name};" self.current_line += f"&{name};"
def html_to_minetest(html, formspec_version=6, include_images=True): def html_to_minetest(html, formspec_version=7, include_images=True):
parser = MinetestHTMLParser(include_images) parser = MinetestHTMLParser(include_images)
parser.feed(html) parser.feed(html)
parser.finish_line() parser.finish_line()
@ -200,3 +204,70 @@ def html_to_minetest(html, formspec_version=6, include_images=True):
"images": parser.images, "images": parser.images,
"image_tooltips": parser.image_tooltips, "image_tooltips": parser.image_tooltips,
} }
def package_info_as_hypertext(package: Package, formspec_version: int = 7):
body = ""
def add_value(label, value):
nonlocal body
body += f"{label}\n<b>{value}</b>\n\n"
def add_list(label, items):
nonlocal body
body += label + "\n<b>"
for i, item in enumerate(items):
if i != 0:
body += "</b>, <b>"
body += item
if len(items) == 0:
body += "-"
body += "</b>\n\n"
add_value(gettext("Type"), package.type.text)
add_list(gettext("Tags"), [tag.title for tag in package.tags])
if package.type != PackageType.GAME:
[supported, unsupported] = package.get_sorted_game_support_pair()
supports_all_games = package.supports_all_games or len(supported) == 0
if supports_all_games:
add_value(gettext("Supported Games"), gettext("No specific game required"))
else:
add_list(gettext("Supported Games"), [support.game.title for support in supported])
if unsupported and supports_all_games:
add_list(gettext("Unsupported Games"), [support.game.title for support in supported])
if package.type != PackageType.TXP:
add_list(gettext("Dependencies"), [x.meta_package.name for x in package.get_sorted_hard_dependencies()])
add_list(gettext("Optional dependencies"), [x.meta_package.name for x in package.get_sorted_optional_dependencies()])
languages = [trans.language.title for trans in package.translations]
languages.insert(0, "English")
add_list(gettext("Languages"), languages)
if package.license == package.media_license:
license = package.license.name
elif package.type == package.type.TXP:
license = package.media_license.name
else:
license = gettext("%(code_license)s for code,<br>%(media_license)s for media.",
code_license=package.license.name, media_license=package.media_license.name).replace("<br>", " ")
add_value(gettext("License"), license)
if package.dev_state:
add_value(gettext("Maintenance State"), package.dev_state.value)
add_value(gettext("Added"), package.created_at)
add_list(gettext("Maintainers"), [user.display_name for user in package.maintainers])
add_list(gettext("Provides"), [x.name for x in package.provides])
return {
"head": HEAD,
"body": body,
"links": {},
"images": {},
"image_tooltips": {},
}