mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-08 22:17:34 +01:00
Add more screenshot APIs
This commit is contained in:
parent
509f03ce65
commit
e1fe63ab19
@ -20,12 +20,12 @@ from sqlalchemy.sql.expression import func
|
|||||||
|
|
||||||
from app import csrf
|
from app import csrf
|
||||||
from app.markdown import render_markdown
|
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.querybuilder import QueryBuilder
|
||||||
from app.utils import is_package_page
|
from app.utils import is_package_page
|
||||||
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, api_order_screenshots
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/packages/")
|
@bp.route("/api/packages/")
|
||||||
@ -147,13 +147,6 @@ def package_dependencies(package):
|
|||||||
return jsonify(out)
|
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/")
|
@bp.route("/api/topics/")
|
||||||
def topics():
|
def topics():
|
||||||
qb = QueryBuilder(request.args)
|
qb = QueryBuilder(request.args)
|
||||||
@ -200,6 +193,13 @@ def markdown():
|
|||||||
return render_markdown(request.data.decode("utf-8"))
|
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"])
|
@bp.route("/api/packages/<author>/<name>/releases/new/", methods=["POST"])
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@is_package_page
|
@is_package_page
|
||||||
@ -233,6 +233,13 @@ def create_release(token, package):
|
|||||||
return api_create_zip_release(token, package, data["title"], file)
|
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"])
|
@bp.route("/api/packages/<author>/<name>/screenshots/new/", methods=["POST"])
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@is_package_page
|
@is_package_page
|
||||||
@ -253,3 +260,62 @@ def create_screenshot(token: APIToken, package: Package):
|
|||||||
error(400, "Missing 'file' in multipart body")
|
error(400, "Missing 'file' in multipart body")
|
||||||
|
|
||||||
return api_create_screenshot(token, package, data["title"], file)
|
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 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.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
|
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,
|
"success": True,
|
||||||
"screenshot": ss.getAsDictionary()
|
"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 app.utils import *
|
||||||
from . import bp
|
from . import bp
|
||||||
from app.logic.LogicError import LogicError
|
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):
|
class CreateScreenshotForm(FlaskForm):
|
||||||
@ -61,17 +61,11 @@ def screenshots(package):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
order = request.form.get("order")
|
order = request.form.get("order")
|
||||||
if order:
|
if order:
|
||||||
lookup = {}
|
try:
|
||||||
for screenshot in package.screenshots:
|
do_order_screenshots(current_user, package, order.split(","))
|
||||||
lookup[str(screenshot.id)] = screenshot
|
return redirect(package.getDetailsURL())
|
||||||
|
except LogicError as e:
|
||||||
counter = 1
|
flash(e.message, "danger")
|
||||||
for id in order.split(","):
|
|
||||||
lookup[id].order = counter
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(package.getDetailsURL())
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
form.populate_obj(package)
|
form.populate_obj(package)
|
||||||
|
@ -16,9 +16,9 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
|||||||
### Misc
|
### Misc
|
||||||
|
|
||||||
* GET `/api/whoami/` - Json dictionary with the following keys:
|
* GET `/api/whoami/` - Json dictionary with the following keys:
|
||||||
* `is_authenticated` - True on successful API authentication
|
* `is_authenticated` - True on successful API authentication
|
||||||
* `username` - Username of the user authenticated as, null otherwise.
|
* `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.
|
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
|
||||||
|
|
||||||
### Packages
|
### 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/scores/` - See [Package Queries](#package-queries)
|
||||||
* GET `/api/packages/<username>/<name>/`
|
* GET `/api/packages/<username>/<name>/`
|
||||||
* GET `/api/packages/<username>/<name>/dependencies/`
|
* 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:
|
* GET `/api/tags/` - List of:
|
||||||
* `name` - technical name
|
* `name` - technical name
|
||||||
* `title` - human-readable title
|
* `title` - human-readable title
|
||||||
* `description` - tag description or null
|
* `description` - tag description or null
|
||||||
* GET `/api/homepage/`
|
* GET `/api/homepage/`
|
||||||
* `count` - number of packages
|
* `count` - number of packages
|
||||||
* `downloads` - get number of downloads
|
* `downloads` - get number of downloads
|
||||||
* `new` - new packages
|
* `new` - new packages
|
||||||
* `updated` - recently updated packages
|
* `updated` - recently updated packages
|
||||||
* `pop_mod` - popular mods
|
* `pop_mod` - popular mods
|
||||||
* `pop_txp` - popular textures
|
* `pop_txp` - popular textures
|
||||||
* `pop_game` - popular games
|
* `pop_game` - popular games
|
||||||
* `high_reviewed` - highest reviewed
|
* `high_reviewed` - highest reviewed
|
||||||
|
|
||||||
### Releases
|
### Releases
|
||||||
|
|
||||||
* GET `/api/packages/<username>/<name>/releases/` (List)
|
* GET `/api/packages/<username>/<name>/releases/` (List)
|
||||||
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
|
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
|
||||||
* Requires authentication.
|
* Requires authentication.
|
||||||
* Body is multipart form if zip upload, JSON otherwise.
|
* Body is multipart form if zip upload, JSON otherwise.
|
||||||
* `title`: human-readable name of the release.
|
* `title`: human-readable name of the release.
|
||||||
* For Git release creation:
|
* For Git release creation:
|
||||||
* `method`: must be `git`.
|
* `method`: must be `git`.
|
||||||
* `ref`: (Optional) git reference, eg: `master`.
|
* `ref`: (Optional) git reference, eg: `master`.
|
||||||
* For zip upload release creation:
|
* For zip upload release creation:
|
||||||
* `file`: multipart file to upload, like `<input type=file>`.
|
* `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/).
|
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create release from Git
|
# Create release from Git
|
||||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||||
-d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }"
|
-d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }"
|
||||||
|
|
||||||
# Create release from zip upload
|
# Create release from zip upload
|
||||||
curl https://content.minetest.net/api/packages/username/name/releases/new/ \
|
curl https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||||
-H "Authorization: Bearer YOURTOKEN" \
|
-H "Authorization: Bearer YOURTOKEN" \
|
||||||
-F title="My Release" -F file=@path/to/file.zip
|
-F title="My Release" -F file=@path/to/file.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Screenshots
|
### 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)
|
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
|
||||||
* Requires authentication.
|
* Requires authentication.
|
||||||
* Body is multipart form data.
|
* Body is multipart form data.
|
||||||
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
|
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
|
||||||
* `file`: multipart file to upload, like `<input type=file>`.
|
* `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
|
```bash
|
||||||
|
# Create screenshots
|
||||||
curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||||
-H "Authorization: Bearer YOURTOKEN" \
|
-H "Authorization: Bearer YOURTOKEN" \
|
||||||
-F title="My Release" -F file=@path/to/screnshot.png
|
-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
|
### Topics
|
||||||
|
|
||||||
* GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options:
|
* GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options:
|
||||||
* `show_added` - Show topics which exist as packages, default true.
|
* `show_added` - Show topics which exist as packages, default true.
|
||||||
* `show_discarded` - Show topics which have been marked as outdated, default false.
|
* `show_discarded` - Show topics which have been marked as outdated, default false.
|
||||||
|
|
||||||
### Minetest
|
### Minetest
|
||||||
|
|
||||||
@ -100,7 +125,7 @@ curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
|||||||
|
|
||||||
Example:
|
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:
|
Supported query parameters:
|
||||||
|
|
||||||
@ -116,15 +141,15 @@ Supported query parameters:
|
|||||||
* `protocol_version` - Only show packages supported by this Minetest protocol version.
|
* `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`.
|
* `engine_version` - Only show packages supported by this Minetest engine version, eg: `5.3.0`.
|
||||||
* `fmt` - How the response is formated.
|
* `fmt` - How the response is formated.
|
||||||
* `keys` - author/name only.
|
* `keys` - author/name only.
|
||||||
* `short` - stuff needed for the Minetest client.
|
* `short` - stuff needed for the Minetest client.
|
||||||
|
|
||||||
|
|
||||||
## Topic Queries
|
## Topic Queries
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
/api/topics/?q=mobs
|
/api/topics/?q=mobs
|
||||||
|
|
||||||
Supported query parameters:
|
Supported query parameters:
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
|
from app.logic.LogicError import LogicError
|
||||||
from app.logic.uploads import upload_file
|
from app.logic.uploads import upload_file
|
||||||
from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db
|
from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db
|
||||||
from app.utils import addNotification
|
from app.utils import addNotification
|
||||||
@ -27,3 +28,21 @@ def do_create_screenshot(user: User, package: Package, title: str, file):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return ss
|
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):
|
def getThumbnailURL(self, level=2):
|
||||||
return self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level))
|
return self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level))
|
||||||
|
|
||||||
def getAsDictionary(self):
|
def getAsDictionary(self, base_url=""):
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
"order": self.order,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"url": self.url,
|
"url": base_url + self.url,
|
||||||
"approved": self.approved,
|
"approved": self.approved,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user