Add more screenshot APIs

This commit is contained in:
rubenwardy 2021-02-02 17:09:25 +00:00
parent 509f03ce65
commit e1fe63ab19
6 changed files with 181 additions and 65 deletions

@ -20,12 +20,12 @@ from sqlalchemy.sql.expression import func
from app import csrf
from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Tags, Permission, ForumTopic, MinetestRelease, APIToken
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Tags, Permission, ForumTopic, MinetestRelease, APIToken, PackageScreenshot
from app.querybuilder import QueryBuilder
from app.utils import is_package_page
from . import bp
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, api_order_screenshots
@bp.route("/api/packages/")
@ -147,13 +147,6 @@ def package_dependencies(package):
return jsonify(out)
@bp.route("/api/packages/<author>/<name>/releases/")
@is_package_page
def list_releases(package):
releases = package.releases.filter_by(approved=True).all()
return jsonify([ rel.getAsDictionary() for rel in releases ])
@bp.route("/api/topics/")
def topics():
qb = QueryBuilder(request.args)
@ -200,6 +193,13 @@ def markdown():
return render_markdown(request.data.decode("utf-8"))
@bp.route("/api/packages/<author>/<name>/releases/")
@is_package_page
def list_releases(package):
releases = package.releases.filter_by(approved=True).all()
return jsonify([ rel.getAsDictionary() for rel in releases ])
@bp.route("/api/packages/<author>/<name>/releases/new/", methods=["POST"])
@csrf.exempt
@is_package_page
@ -233,6 +233,13 @@ def create_release(token, package):
return api_create_zip_release(token, package, data["title"], file)
@bp.route("/api/packages/<author>/<name>/screenshots/")
@is_package_page
def list_screenshots(package):
screenshots = package.screenshots.all()
return jsonify([ss.getAsDictionary(current_app.config["BASE_URL"]) for ss in screenshots])
@bp.route("/api/packages/<author>/<name>/screenshots/new/", methods=["POST"])
@csrf.exempt
@is_package_page
@ -253,3 +260,62 @@ def create_screenshot(token: APIToken, package: Package):
error(400, "Missing 'file' in multipart body")
return api_create_screenshot(token, package, data["title"], file)
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
@is_package_page
def screenshot(package, id):
ss = PackageScreenshot.query.get(id)
if ss is None or ss.package != package:
error(404, "Screenshot not found")
return jsonify(ss.getAsDictionary(current_app.config["BASE_URL"]))
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/", methods=["DELETE"])
@csrf.exempt
@is_package_page
@is_api_authd
def delete_screenshot(token: APIToken, package: Package, id: int):
ss = PackageScreenshot.query.get(id)
if ss is None or ss.package != package:
error(404, "Screenshot not found")
if not token:
error(401, "Authentication needed")
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
error(403, "You do not have the permission to delete screenshots")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
if package.cover_image == ss:
package.cover_image = None
db.session.merge(package)
db.session.delete(ss)
db.session.commit()
return jsonify({ "success": True })
@bp.route("/api/packages/<author>/<name>/screenshots/order/", methods=["POST"])
@csrf.exempt
@is_package_page
@is_api_authd
def order_screenshots(token: APIToken, package: Package):
if not token:
error(401, "Authentication needed")
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
error(403, "You do not have the permission to delete screenshots")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
json = request.json
if json is None or not isinstance(json, list):
error(400, "Expected order body to be array")
return api_order_screenshots(token, package, request.json)

@ -17,7 +17,7 @@
from flask import jsonify, abort, make_response, url_for
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
@ -69,3 +69,14 @@ def api_create_screenshot(token: APIToken, package: Package, title: str, file):
"success": True,
"screenshot": ss.getAsDictionary()
})
def api_order_screenshots(token: APIToken, package: Package, order: [any]):
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
run_safe(do_order_screenshots, token.owner, package, order)
return jsonify({
"success": True
})

@ -25,7 +25,7 @@ from wtforms.validators import *
from app.utils import *
from . import bp
from app.logic.LogicError import LogicError
from app.logic.screenshots import do_create_screenshot
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
class CreateScreenshotForm(FlaskForm):
@ -61,17 +61,11 @@ def screenshots(package):
if request.method == "POST":
order = request.form.get("order")
if order:
lookup = {}
for screenshot in package.screenshots:
lookup[str(screenshot.id)] = screenshot
counter = 1
for id in order.split(","):
lookup[id].order = counter
counter += 1
db.session.commit()
return redirect(package.getDetailsURL())
try:
do_order_screenshots(current_user, package, order.split(","))
return redirect(package.getDetailsURL())
except LogicError as e:
flash(e.message, "danger")
if form.validate_on_submit():
form.populate_obj(package)

@ -16,9 +16,9 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
### Misc
* GET `/api/whoami/` - Json dictionary with the following keys:
* `is_authenticated` - True on successful API authentication
* `username` - Username of the user authenticated as, null otherwise.
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
* `is_authenticated` - True on successful API authentication
* `username` - Username of the user authenticated as, null otherwise.
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
### Packages
@ -26,70 +26,95 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* GET `/api/scores/` - See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/`
* GET `/api/packages/<username>/<name>/dependencies/`
* If query argument `only_hard` is present, only hard deps will be returned.
* If query argument `only_hard` is present, only hard deps will be returned.
* GET `/api/tags/` - List of:
* `name` - technical name
* `title` - human-readable title
* `description` - tag description or null
* `name` - technical name
* `title` - human-readable title
* `description` - tag description or null
* GET `/api/homepage/`
* `count` - number of packages
* `downloads` - get number of downloads
* `new` - new packages
* `updated` - recently updated packages
* `pop_mod` - popular mods
* `pop_txp` - popular textures
* `pop_game` - popular games
* `high_reviewed` - highest reviewed
* `count` - number of packages
* `downloads` - get number of downloads
* `new` - new packages
* `updated` - recently updated packages
* `pop_mod` - popular mods
* `pop_txp` - popular textures
* `pop_game` - popular games
* `high_reviewed` - highest reviewed
### Releases
* GET `/api/packages/<username>/<name>/releases/` (List)
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
* Requires authentication.
* Body is multipart form if zip upload, JSON otherwise.
* `title`: human-readable name of the release.
* For Git release creation:
* `method`: must be `git`.
* `ref`: (Optional) git reference, eg: `master`.
* For zip upload release creation:
* `file`: multipart file to upload, like `<input type=file>`.
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
* Requires authentication.
* Body is multipart form if zip upload, JSON otherwise.
* `title`: human-readable name of the release.
* For Git release creation:
* `method`: must be `git`.
* `ref`: (Optional) git reference, eg: `master`.
* For zip upload release creation:
* `file`: multipart file to upload, like `<input type=file>`.
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
Examples:
```bash
# Create release from Git
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }"
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }"
# Create release from zip upload
curl https://content.minetest.net/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip
```
### Screenshots
* GET `/api/packages/<username>/<name>/screenshots/` (List)
* Returns array of screenshot dictionaries with keys:
* `id`: screenshot ID
* `approved`: true if approved and visible.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `url`: absolute URL to screenshot.
* `order`: Number used in ordering.
* GET `/api/packages/<username>/<name>/screenshots/<id>/` (Read)
* Returns screenshot dictionary like above.
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
* Requires authentication.
* Body is multipart form data.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `file`: multipart file to upload, like `<input type=file>`.
* Requires authentication.
* Body is multipart form data.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `file`: multipart file to upload, like `<input type=file>`.
* DELETE `/api/packages/<username>/<name>/screenshots/<id>/` (Delete)
* Requires authentication.
* Deletes screenshot.
* POST `/api/packages/<username>/<name>/screenshots/order/`
* Requires authentication.
* Body is a JSON array containing the screenshot IDs in their order.
Example:
Examples:
```bash
# Create screenshots
curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png
# Delete screenshot
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
-H "Authorization: Bearer YOURTOKEN"
# Reorder screenshots
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]"
```
### Topics
* GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options:
* `show_added` - Show topics which exist as packages, default true.
* `show_discarded` - Show topics which have been marked as outdated, default false.
* `show_added` - Show topics which exist as packages, default true.
* `show_discarded` - Show topics which have been marked as outdated, default false.
### Minetest
@ -100,7 +125,7 @@ curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
Example:
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
Supported query parameters:
@ -116,15 +141,15 @@ Supported query parameters:
* `protocol_version` - Only show packages supported by this Minetest protocol version.
* `engine_version` - Only show packages supported by this Minetest engine version, eg: `5.3.0`.
* `fmt` - How the response is formated.
* `keys` - author/name only.
* `short` - stuff needed for the Minetest client.
* `keys` - author/name only.
* `short` - stuff needed for the Minetest client.
## Topic Queries
Example:
/api/topics/?q=mobs
/api/topics/?q=mobs
Supported query parameters:

@ -1,5 +1,6 @@
from werkzeug.exceptions import abort
from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file
from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db
from app.utils import addNotification
@ -27,3 +28,21 @@ def do_create_screenshot(user: User, package: Package, title: str, file):
db.session.commit()
return ss
def do_order_screenshots(_user: User, package: Package, order: [any]):
lookup = {}
for screenshot in package.screenshots.all():
lookup[screenshot.id] = screenshot
counter = 1
for id in order:
try:
lookup[int(id)].order = counter
counter += 1
except KeyError as e:
raise LogicError(400, "Unable to find screenshot with id={}".format(id))
except ValueError as e:
raise LogicError(400, "Invalid number: {}".format(id))
db.session.commit()

@ -909,11 +909,12 @@ class PackageScreenshot(db.Model):
def getThumbnailURL(self, level=2):
return self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level))
def getAsDictionary(self):
def getAsDictionary(self, base_url=""):
return {
"id": self.id,
"order": self.order,
"title": self.title,
"url": self.url,
"url": base_url + self.url,
"approved": self.approved,
}