Compare commits
243 Commits
game_suppo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ffbe93e0f | ||
|
|
0902d39970 | ||
|
|
8d5ba2af72 | ||
|
|
34948770ce | ||
|
|
8bafaed671 | ||
|
|
a604b3cd09 | ||
|
|
ad41bc01b9 | ||
|
|
4db70bf401 | ||
|
|
b88cc1366f | ||
|
|
feeed21b94 | ||
|
|
dfa4e5a7a3 | ||
|
|
28e5f44a30 | ||
|
|
b03b5b1adb | ||
|
|
b7c6c3f338 | ||
|
|
8568830cf6 | ||
|
|
1ac3bd1da8 | ||
|
|
9778f8be53 | ||
|
|
7e47287b23 | ||
|
|
7d8e45f64f | ||
|
|
8f77197cde | ||
|
|
1f24fe0843 | ||
|
|
c15e1eb042 | ||
|
|
1e6499d23a | ||
|
|
19257d4dc0 | ||
|
|
762562f837 | ||
|
|
75192abb3a | ||
|
|
6276dfb614 | ||
|
|
33860b84cf | ||
|
|
607065d94c | ||
|
|
ac02e2eb2e | ||
|
|
ce5ae3fe40 | ||
|
|
fb87b51a23 | ||
|
|
7070644842 | ||
|
|
ced9f320a4 | ||
|
|
fb8264bf7d | ||
|
|
cfa1bee2d8 | ||
|
|
fa11b0ffa8 | ||
|
|
c3889d64a3 | ||
|
|
8b69d552fd | ||
|
|
0a1ff05c39 | ||
|
|
2b33a89107 | ||
|
|
bde76b5b46 | ||
|
|
7c4e6aece8 | ||
|
|
e0e50a78ed | ||
|
|
930a460ef3 | ||
|
|
f7d81f9fba | ||
|
|
25998fdcc4 | ||
|
|
617e7f043b | ||
|
|
ced7abd27f | ||
|
|
f29a1be1eb | ||
|
|
0704ffb1a2 | ||
|
|
9f6c295484 | ||
|
|
8936bdca81 | ||
|
|
f86b1343b7 | ||
|
|
d6e39fb896 | ||
|
|
4004c16504 | ||
|
|
66b935037c | ||
|
|
4c78e098cf | ||
|
|
864add055b | ||
|
|
e44a5545a3 | ||
|
|
30aecb8565 | ||
|
|
79354453e5 | ||
|
|
d6e4abec73 | ||
|
|
be03243250 | ||
|
|
92f935c6de | ||
|
|
0a52e26cd0 | ||
|
|
6bf94e558a | ||
|
|
9ec215c3d4 | ||
|
|
704c6be1c4 | ||
|
|
0adf02bf99 | ||
|
|
8631425ff7 | ||
|
|
b8e25f8565 | ||
|
|
394b1fe33d | ||
|
|
038e65bfe3 | ||
|
|
6e2d8b1974 | ||
|
|
310f1baa09 | ||
|
|
acf9e16234 | ||
|
|
741bd23144 | ||
|
|
bdf1c2df6e | ||
|
|
8d1268bd19 | ||
|
|
8db72faf3c | ||
|
|
e4c061858e | ||
|
|
0653ed2183 | ||
|
|
a9f82b6e1b | ||
|
|
80d06d154a | ||
|
|
6265c0665b | ||
|
|
1c2a56e784 | ||
|
|
e03c8f04e1 | ||
|
|
bd23a99aec | ||
|
|
26272ce793 | ||
|
|
66f918c1bf | ||
|
|
a6fccc7c58 | ||
|
|
ae05c10e7c | ||
|
|
14d8ef6cb1 | ||
|
|
bc62c2b1be | ||
|
|
dedafe9c71 | ||
|
|
e754d8d80d | ||
|
|
cbb59e5e55 | ||
|
|
1928a2302c | ||
|
|
6b3a2a0fe7 | ||
|
|
52802f44f6 | ||
|
|
f01adc4cb4 | ||
|
|
c21dc5313d | ||
|
|
5243176d74 | ||
|
|
c495fcbd1a | ||
|
|
036a55e61e | ||
|
|
21ef5f9b84 | ||
|
|
2ddcbfb5ab | ||
|
|
c931c78b6a | ||
|
|
815d812297 | ||
|
|
8ed86b53ca | ||
|
|
98f27364f2 | ||
|
|
4e502f38aa | ||
|
|
3a468a9b85 | ||
|
|
a6009654c7 | ||
|
|
2d8660902d | ||
|
|
2e5ced23a8 | ||
|
|
4011cc56b6 | ||
|
|
1543965e5f | ||
|
|
c21a56585f | ||
|
|
cd53696831 | ||
|
|
8777d2bfd3 | ||
|
|
00cf79224d | ||
|
|
5b0d42173f | ||
|
|
6891ee8b19 | ||
|
|
5f2b2ffdf1 | ||
|
|
f0e67c93d6 | ||
|
|
e5fd908b54 | ||
|
|
3af7a19563 | ||
|
|
e1f0792dce | ||
|
|
2e91656245 | ||
|
|
8378095343 | ||
|
|
b6f67d4b0e | ||
|
|
ffe808c915 | ||
|
|
6169f4c0e4 | ||
|
|
8c5e542268 | ||
|
|
893b902314 | ||
|
|
2435e8e3d0 | ||
|
|
628d44460d | ||
|
|
fc1b7e500d | ||
|
|
7cfbbbe7e6 | ||
|
|
bea743b536 | ||
|
|
6ccc575cb4 | ||
|
|
9e4be57754 | ||
|
|
82b47628ae | ||
|
|
3bd62f4184 | ||
|
|
9a98c2f6c2 | ||
|
|
73c1706e6a | ||
|
|
649ee8bcd6 | ||
|
|
37e8f2dc28 | ||
|
|
4d9628a156 | ||
|
|
0ff9a3838e | ||
|
|
24eacb191d | ||
|
|
23e9ad6ef5 | ||
|
|
c7f26f706d | ||
|
|
699eabef80 | ||
|
|
73376194e0 | ||
|
|
aafa56df95 | ||
|
|
978c5d9704 | ||
|
|
c332e8f940 | ||
|
|
f116259f6a | ||
|
|
3f9902b001 | ||
|
|
a627276ab4 | ||
|
|
f72a66816a | ||
|
|
508f7d7e2b | ||
|
|
b86d372bd2 | ||
|
|
c75fd51626 | ||
|
|
6ad12288c3 | ||
|
|
af2543a99e | ||
|
|
2c61032d15 | ||
|
|
a54104aa82 | ||
|
|
dd2e73b40f | ||
|
|
a5ac4f38cf | ||
|
|
2ff11dec0a | ||
|
|
8e1547ca3b | ||
|
|
757e182d1b | ||
|
|
5562ca6039 | ||
|
|
74cf577245 | ||
|
|
79387309d8 | ||
|
|
e4b81feb5c | ||
|
|
58ac57e098 | ||
|
|
abc2941756 | ||
|
|
1432384b63 | ||
|
|
52df207088 | ||
|
|
7f834dbf8c | ||
|
|
9131b29b48 | ||
|
|
f621cd13d2 | ||
|
|
69904dbe81 | ||
|
|
d56430c0f0 | ||
|
|
f69bc8fc1e | ||
|
|
5a173ee18b | ||
|
|
6429b2e26d | ||
|
|
93f36adfea | ||
|
|
25547c9f38 | ||
|
|
6425149d20 | ||
|
|
4738e11ed0 | ||
|
|
ae67f6ce79 | ||
|
|
c23f004d35 | ||
|
|
8effec2cbb | ||
|
|
5afc429c25 | ||
|
|
d5552ad517 | ||
|
|
65a14ffdf1 | ||
|
|
837d0b5bc1 | ||
|
|
5b1417f432 | ||
|
|
53a004c41c | ||
|
|
ac34939c99 | ||
|
|
9aa8886309 | ||
|
|
1166cca357 | ||
|
|
395d3dd16b | ||
|
|
009dfd07de | ||
|
|
ff07ff5b7f | ||
|
|
2b32cfe6fa | ||
|
|
b31e9e71b6 | ||
|
|
94bf1973a0 | ||
|
|
357dfe76e8 | ||
|
|
5da955b3a5 | ||
|
|
7584a867eb | ||
|
|
9c77212f4a | ||
|
|
2b62224a5b | ||
|
|
bb561104f8 | ||
|
|
bdd9ab6a29 | ||
|
|
d450d6bae3 | ||
|
|
02cc464098 | ||
|
|
563345eddd | ||
|
|
e5e3230a16 | ||
|
|
ed4d4c67d9 | ||
|
|
42df276e73 | ||
|
|
bd17080f2a | ||
|
|
2fb7ddaaee | ||
|
|
ef868f776c | ||
|
|
06979345c7 | ||
|
|
e5de270e65 | ||
|
|
031c3c4684 | ||
|
|
a9d31590e8 | ||
|
|
4c4a55872a | ||
|
|
9387db5f8d | ||
|
|
6a5c7d44bf | ||
|
|
f1ace7fce8 | ||
|
|
20c946d127 | ||
|
|
bb2a1f3638 | ||
|
|
e603f29b47 | ||
|
|
9c2ecd1e22 | ||
|
|
80c3416ca7 |
2
.github/SECURITY.md
vendored
2
.github/SECURITY.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest production version, deployed to <https://content.minetest.net>.
|
||||
We only support the latest production version, deployed to <https://content.luanti.org>.
|
||||
This is usually the latest `master` commit.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -6,6 +6,8 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install docker-compose
|
||||
run: sudo apt-get install -y docker-compose
|
||||
- uses: actions/checkout@v4
|
||||
- name: Copy config
|
||||
run: cp utils/ci/* .
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,16 +1,20 @@
|
||||
FROM python:3.10.11
|
||||
FROM python:3.10.11-alpine
|
||||
|
||||
RUN groupadd -g 5123 cdb && \
|
||||
useradd -r -u 5123 -g cdb cdb
|
||||
RUN addgroup --gid 5123 cdb && \
|
||||
adduser --uid 5123 -S cdb -G cdb
|
||||
|
||||
WORKDIR /home/cdb
|
||||
|
||||
RUN \
|
||||
apk add --no-cache postgresql-libs git bash unzip && \
|
||||
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev g++
|
||||
|
||||
RUN mkdir /var/cdb
|
||||
RUN chown -R cdb:cdb /var/cdb
|
||||
|
||||
COPY requirements.lock.txt requirements.lock.txt
|
||||
RUN pip install -r requirements.lock.txt
|
||||
RUN pip install gunicorn
|
||||
RUN pip install -r requirements.lock.txt && \
|
||||
pip install gunicorn
|
||||
|
||||
COPY utils utils
|
||||
COPY config.cfg config.cfg
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ContentDB
|
||||

|
||||

|
||||
|
||||
A content database for Minetest mods, games, and more.\
|
||||
A content database for Luanti mods, games, and more.\
|
||||
Developed by rubenwardy, license AGPLv3.0+.
|
||||
|
||||
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
|
||||
@@ -82,7 +82,7 @@ Package "1" --> "*" Release
|
||||
Package "1" --> "*" Dependency
|
||||
Package "1" --> "*" Tag
|
||||
Package "1" --> "*" MetaPackage : provides
|
||||
Release --> MinetestVersion
|
||||
Release --> LuantiVersion
|
||||
Package --> License
|
||||
Dependency --> Package
|
||||
Dependency --> MetaPackage
|
||||
|
||||
@@ -21,13 +21,12 @@ import redis
|
||||
from flask import redirect, url_for, render_template, flash, request, Flask, send_from_directory, make_response, render_template_string
|
||||
from flask_babel import Babel, gettext
|
||||
from flask_flatpages import FlatPages
|
||||
from flask_flatpages.utils import pygmented_markdown
|
||||
from flask_github import GitHub
|
||||
from flask_login import logout_user, current_user, LoginManager
|
||||
from flask_mail import Mail
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from app.markdown import init_markdown, MARKDOWN_EXTENSIONS, MARKDOWN_EXTENSION_CONFIG
|
||||
from app.markdown import init_markdown, render_markdown
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
@@ -67,19 +66,18 @@ app = Flask(__name__, static_folder="public/static")
|
||||
def my_flatpage_renderer(text):
|
||||
# Render with jinja first
|
||||
prerendered_body = render_template_string(text)
|
||||
return pygmented_markdown(prerendered_body, flatpages=pages)
|
||||
return render_markdown(prerendered_body, clean=False)
|
||||
|
||||
|
||||
app.config["FLATPAGES_ROOT"] = "flatpages"
|
||||
app.config["FLATPAGES_EXTENSION"] = ".md"
|
||||
app.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = MARKDOWN_EXTENSIONS
|
||||
app.config["FLATPAGES_EXTENSION_CONFIG"] = MARKDOWN_EXTENSION_CONFIG
|
||||
app.config["FLATPAGES_HTML_RENDERER"] = my_flatpage_renderer
|
||||
app.config["WTF_CSRF_TIME_LIMIT"] = None
|
||||
|
||||
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "../translations"
|
||||
app.config["LANGUAGES"] = {
|
||||
"en": "English",
|
||||
"cs": "čeština",
|
||||
"de": "Deutsch",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
@@ -90,6 +88,7 @@ app.config["LANGUAGES"] = {
|
||||
"ru": "русский язык",
|
||||
"sk": "Slovenčina",
|
||||
"sv": "Svenska",
|
||||
"ta": "தமிழ்",
|
||||
"tr": "Türkçe",
|
||||
"uk": "Українська",
|
||||
"vi": "tiếng Việt",
|
||||
|
||||
@@ -1,248 +1,252 @@
|
||||
# THIS FILE IS AUTOGENERATED: utils/extract_translations.py
|
||||
|
||||
from flask_babel import gettext
|
||||
from flask_babel import pgettext
|
||||
|
||||
# NOTE: tags: title for server_tools
|
||||
pgettext("tags", "Server Moderation and Tools")
|
||||
# NOTE: tags: description for server_tools
|
||||
pgettext("tags", "Helps with server maintenance and moderation")
|
||||
# NOTE: tags: title for pvp
|
||||
pgettext("tags", "Player vs Player (PvP)")
|
||||
# NOTE: tags: description for pvp
|
||||
pgettext("tags", "Designed to be played competitively against other players")
|
||||
# NOTE: tags: title for player_effects
|
||||
pgettext("tags", "Player Effects / Power Ups")
|
||||
# NOTE: tags: description for player_effects
|
||||
pgettext("tags", "For content that changes player effects, including physics, for example: speed, jump height or gravity.")
|
||||
# NOTE: tags: title for jam_game_2023
|
||||
pgettext("tags", "Jam / Game 2023")
|
||||
# NOTE: tags: description for jam_game_2023
|
||||
pgettext("tags", "Entries to the 2023 Minetest Game Jam ")
|
||||
# NOTE: tags: title for mapgen
|
||||
pgettext("tags", "Mapgen / Biomes / Decoration")
|
||||
# NOTE: tags: description for mapgen
|
||||
pgettext("tags", "New mapgen or changes mapgen")
|
||||
# NOTE: tags: title for world_tools
|
||||
pgettext("tags", "World Maintenance and Tools")
|
||||
# NOTE: tags: description for world_tools
|
||||
pgettext("tags", "Tools to manage the world")
|
||||
# NOTE: tags: title for inventory
|
||||
pgettext("tags", "Inventory")
|
||||
# NOTE: tags: description for inventory
|
||||
pgettext("tags", "Changes the inventory GUI")
|
||||
# NOTE: tags: title for developer_tools
|
||||
pgettext("tags", "Developer Tools")
|
||||
# NOTE: tags: description for developer_tools
|
||||
pgettext("tags", "Tools for game and mod developers")
|
||||
# NOTE: tags: title for april_fools
|
||||
pgettext("tags", "Joke")
|
||||
# NOTE: tags: description for april_fools
|
||||
pgettext("tags", "For humorous content, meant as a novelty or joke, not to be taken seriously, and that is not meant to be used seriously or long-term.")
|
||||
# NOTE: tags: title for singleplayer
|
||||
pgettext("tags", "Singleplayer-focused")
|
||||
# NOTE: tags: description for singleplayer
|
||||
pgettext("tags", "Content that can be played alone")
|
||||
# NOTE: tags: title for crafting
|
||||
pgettext("tags", "Crafting")
|
||||
# NOTE: tags: description for crafting
|
||||
pgettext("tags", "Big changes to crafting gameplay")
|
||||
# NOTE: tags: title for adventure__rpg
|
||||
pgettext("tags", "Adventure / RPG")
|
||||
# NOTE: tags: title for shooter
|
||||
pgettext("tags", "Shooter")
|
||||
# NOTE: tags: description for shooter
|
||||
pgettext("tags", "First person shooters (FPS) and more")
|
||||
# NOTE: tags: title for sound_music
|
||||
pgettext("tags", "Sounds / Music")
|
||||
# NOTE: tags: description for sound_music
|
||||
pgettext("tags", "Focuses on or adds new sounds or musical things")
|
||||
# NOTE: tags: title for tools
|
||||
pgettext("tags", "Tools / Weapons / Armor")
|
||||
# NOTE: tags: description for tools
|
||||
pgettext("tags", "Adds or changes tools, weapons, and armor")
|
||||
# NOTE: tags: title for plants_and_farming
|
||||
pgettext("tags", "Plants and Farming")
|
||||
# NOTE: tags: description for plants_and_farming
|
||||
pgettext("tags", "Adds new plants or other farmable resources.")
|
||||
# NOTE: tags: title for simulation
|
||||
pgettext("tags", "Sims")
|
||||
# NOTE: tags: description for simulation
|
||||
pgettext("tags", "Mods and games that aim to simulate real life activity. Similar to SimCity/The Sims/OpenTTD/etc.")
|
||||
# NOTE: tags: title for chat
|
||||
pgettext("tags", "Chat / Commands")
|
||||
# NOTE: tags: description for chat
|
||||
pgettext("tags", "Focus on player chat/communication or console interaction.")
|
||||
# NOTE: tags: title for multiplayer
|
||||
pgettext("tags", "Multiplayer-focused")
|
||||
# NOTE: tags: description for multiplayer
|
||||
pgettext("tags", "Can/should only be used in multiplayer")
|
||||
# NOTE: tags: title for complex_installation
|
||||
pgettext("tags", "Complex installation")
|
||||
# NOTE: tags: description for complex_installation
|
||||
pgettext("tags", "Requires futher installation steps, such as installing LuaRocks or editing the trusted mod setting")
|
||||
# NOTE: tags: title for magic
|
||||
pgettext("tags", "Magic / Enchanting")
|
||||
# NOTE: tags: title for building_mechanics
|
||||
pgettext("tags", "Building Mechanics and Tools")
|
||||
# NOTE: tags: description for building_mechanics
|
||||
pgettext("tags", "Adds game mechanics or tools that change how players build.")
|
||||
# NOTE: tags: title for oneofakind__original
|
||||
pgettext("tags", "One-of-a-kind / Original")
|
||||
# NOTE: tags: description for oneofakind__original
|
||||
pgettext("tags", "For games and such that are of their own kind, distinct and original in nature to others of the same category.")
|
||||
# NOTE: tags: title for building
|
||||
pgettext("tags", "Building")
|
||||
# NOTE: tags: description for building
|
||||
pgettext("tags", "Focuses on building, such as adding new materials or nodes")
|
||||
# NOTE: tags: title for decorative
|
||||
pgettext("tags", "Decorative")
|
||||
# NOTE: tags: description for decorative
|
||||
pgettext("tags", "Adds nodes with no other purpose than for use in building")
|
||||
# NOTE: tags: title for puzzle
|
||||
pgettext("tags", "Puzzle")
|
||||
# NOTE: tags: description for puzzle
|
||||
pgettext("tags", "Focus on puzzle solving instead of combat")
|
||||
# NOTE: tags: title for environment
|
||||
pgettext("tags", "Environment / Weather")
|
||||
# NOTE: tags: description for environment
|
||||
pgettext("tags", "Improves the world, adding weather, ambient sounds, or other environment mechanics")
|
||||
# NOTE: tags: title for less_than_px
|
||||
pgettext("tags", "<16px")
|
||||
# NOTE: tags: description for less_than_px
|
||||
pgettext("tags", "Less than 16px")
|
||||
# NOTE: tags: title for mobs
|
||||
pgettext("tags", "Mobs / Animals / NPCs")
|
||||
# NOTE: tags: description for mobs
|
||||
pgettext("tags", "Adds mobs, animals, and non-player characters")
|
||||
# NOTE: tags: title for custom_mapgen
|
||||
pgettext("tags", "Custom mapgen")
|
||||
# NOTE: tags: description for custom_mapgen
|
||||
pgettext("tags", "Contains a completely custom mapgen implemented in Lua, usually requires worlds to be set to the 'singlenode' mapgen.")
|
||||
# NOTE: tags: title for survival
|
||||
pgettext("tags", "Survival")
|
||||
# NOTE: tags: description for survival
|
||||
pgettext("tags", "Written specifically for survival gameplay with a focus on game-balance, difficulty level, or resources available through crafting, mining, ...")
|
||||
# NOTE: tags: title for education
|
||||
pgettext("tags", "Education")
|
||||
# NOTE: tags: description for education
|
||||
pgettext("tags", "Either has educational value, or is a tool to help teachers ")
|
||||
# NOTE: tags: title for storage
|
||||
pgettext("tags", "Storage")
|
||||
# NOTE: tags: description for storage
|
||||
pgettext("tags", "Adds or improves item storage mechanics")
|
||||
# NOTE: tags: title for library
|
||||
pgettext("tags", "API / Library")
|
||||
# NOTE: tags: description for library
|
||||
pgettext("tags", "Primarily adds an API for other mods to use")
|
||||
# NOTE: tags: title for hud
|
||||
pgettext("tags", "HUD")
|
||||
# NOTE: tags: description for hud
|
||||
pgettext("tags", "For mods that grant the player extra information in the HUD")
|
||||
# NOTE: tags: title for mini-game
|
||||
pgettext("tags", "Mini-game")
|
||||
# NOTE: tags: description for mini-game
|
||||
pgettext("tags", "Adds a mini-game to be played within Minetest")
|
||||
# NOTE: tags: title for pve
|
||||
pgettext("tags", "Player vs Environment (PvE)")
|
||||
# NOTE: tags: description for pve
|
||||
pgettext("tags", "For content designed for one or more players that focus on combat against the world, mobs, or NPCs.")
|
||||
# NOTE: tags: title for sports
|
||||
pgettext("tags", "Sports")
|
||||
# NOTE: tags: title for 16px
|
||||
pgettext("tags", "16px")
|
||||
# NOTE: tags: description for 16px
|
||||
pgettext("tags", "For 16px texture packs")
|
||||
# NOTE: tags: title for 64px
|
||||
pgettext("tags", "64px")
|
||||
# NOTE: tags: description for 64px
|
||||
pgettext("tags", "For 64px texture packs")
|
||||
# NOTE: tags: title for transport
|
||||
pgettext("tags", "Transport")
|
||||
# NOTE: tags: description for transport
|
||||
pgettext("tags", "Adds or changes transportation methods. Includes teleportation, vehicles, ridable mobs, transport infrastructure and thematic content")
|
||||
# NOTE: tags: title for seasonal
|
||||
pgettext("tags", "Seasonal")
|
||||
# NOTE: tags: description for seasonal
|
||||
pgettext("tags", "For content generally themed around a certain season or holiday")
|
||||
# NOTE: tags: title for food
|
||||
pgettext("tags", "Food / Drinks")
|
||||
# NOTE: tags: title for commerce
|
||||
pgettext("tags", "Commerce / Economy")
|
||||
# NOTE: tags: description for commerce
|
||||
pgettext("tags", "Related to economies, money, and trading")
|
||||
# NOTE: tags: title for gui
|
||||
pgettext("tags", "GUI")
|
||||
# NOTE: tags: description for gui
|
||||
pgettext("tags", "For content whose main utility or features are provided within a GUI, on-screen menu, or similar")
|
||||
# NOTE: tags: title for jam_combat_mod
|
||||
pgettext("tags", "Jam / Combat 2020")
|
||||
# NOTE: tags: description for jam_combat_mod
|
||||
pgettext("tags", "For mods created for the Discord \"Combat\" modding event in 2020")
|
||||
# NOTE: tags: title for mtg
|
||||
pgettext("tags", "Minetest Game improved")
|
||||
# NOTE: tags: description for mtg
|
||||
pgettext("tags", "Forks of Minetest Game")
|
||||
# NOTE: tags: title for jam_weekly_2021
|
||||
pgettext("tags", "Jam / Weekly Challenges 2021")
|
||||
# NOTE: tags: description for jam_weekly_2021
|
||||
pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding event in 2021")
|
||||
# NOTE: tags: title for creative
|
||||
pgettext("tags", "Creative")
|
||||
# NOTE: tags: description for creative
|
||||
pgettext("tags", "Written specifically or exclusively for use in creative mode. Adds content only available through a creative inventory, or provides tools that facilitate ingame creation and doesn't add difficulty or scarcity")
|
||||
# NOTE: tags: title for technology
|
||||
pgettext("tags", "Machines / Electronics")
|
||||
# NOTE: tags: description for technology
|
||||
pgettext("tags", "Adds machines useful in automation, tubes, or power.")
|
||||
# NOTE: tags: title for 32px
|
||||
pgettext("tags", "32px")
|
||||
# NOTE: tags: description for 32px
|
||||
pgettext("tags", "For 32px texture packs")
|
||||
# NOTE: tags: title for strategy_rts
|
||||
pgettext("tags", "Strategy / RTS")
|
||||
# NOTE: tags: description for strategy_rts
|
||||
pgettext("tags", "Games and mods with a heavy strategy component, whether real-time or turn-based")
|
||||
# NOTE: tags: title for skins
|
||||
pgettext("tags", "Player customization / Skins")
|
||||
# NOTE: tags: description for skins
|
||||
pgettext("tags", "Allows the player to customize their character by changing the texture or adding accessories.")
|
||||
# NOTE: tags: title for jam_game_2021
|
||||
pgettext("tags", "Jam / Game 2021")
|
||||
# NOTE: tags: description for jam_game_2021
|
||||
pgettext("tags", "Entries to the 2021 Minetest Game Jam")
|
||||
# NOTE: tags: title for 128px
|
||||
pgettext("tags", "128px+")
|
||||
# NOTE: tags: description for 128px
|
||||
pgettext("tags", "For 128px or higher texture packs")
|
||||
# NOTE: tags: title for 16px
|
||||
pgettext("tags", "16px")
|
||||
# NOTE: tags: description for 16px
|
||||
pgettext("tags", "For 16px texture packs")
|
||||
# NOTE: tags: title for 32px
|
||||
pgettext("tags", "32px")
|
||||
# NOTE: tags: description for 32px
|
||||
pgettext("tags", "For 32px texture packs")
|
||||
# NOTE: tags: title for 64px
|
||||
pgettext("tags", "64px")
|
||||
# NOTE: tags: description for 64px
|
||||
pgettext("tags", "For 64px texture packs")
|
||||
# NOTE: tags: title for adventure__rpg
|
||||
pgettext("tags", "Adventure / RPG")
|
||||
# NOTE: tags: title for april_fools
|
||||
pgettext("tags", "Joke")
|
||||
# NOTE: tags: description for april_fools
|
||||
pgettext("tags", "For humorous content, meant as a novelty or joke, not to be taken seriously, and that is not meant to be used seriously or long-term.")
|
||||
# NOTE: tags: title for building
|
||||
pgettext("tags", "Building")
|
||||
# NOTE: tags: description for building
|
||||
pgettext("tags", "Focuses on building, such as adding new materials or nodes")
|
||||
# NOTE: tags: title for building_mechanics
|
||||
pgettext("tags", "Building Mechanics and Tools")
|
||||
# NOTE: tags: description for building_mechanics
|
||||
pgettext("tags", "Adds game mechanics or tools that change how players build.")
|
||||
# NOTE: tags: title for chat
|
||||
pgettext("tags", "Chat / Commands")
|
||||
# NOTE: tags: description for chat
|
||||
pgettext("tags", "Focus on player chat/communication or console interaction.")
|
||||
# NOTE: tags: title for commerce
|
||||
pgettext("tags", "Commerce / Economy")
|
||||
# NOTE: tags: description for commerce
|
||||
pgettext("tags", "Related to economies, money, and trading")
|
||||
# NOTE: tags: title for complex_installation
|
||||
pgettext("tags", "Complex installation")
|
||||
# NOTE: tags: description for complex_installation
|
||||
pgettext("tags", "Requires futher installation steps, such as installing LuaRocks or editing the trusted mod setting")
|
||||
# NOTE: tags: title for crafting
|
||||
pgettext("tags", "Crafting")
|
||||
# NOTE: tags: description for crafting
|
||||
pgettext("tags", "Big changes to crafting gameplay")
|
||||
# NOTE: tags: title for creative
|
||||
pgettext("tags", "Creative")
|
||||
# NOTE: tags: description for creative
|
||||
pgettext("tags", "Written specifically or exclusively for use in creative mode. Adds content only available through a creative inventory, or provides tools that facilitate ingame creation and doesn't add difficulty or scarcity")
|
||||
# NOTE: tags: title for custom_mapgen
|
||||
pgettext("tags", "Custom mapgen")
|
||||
# NOTE: tags: description for custom_mapgen
|
||||
pgettext("tags", "Contains a completely custom mapgen implemented in Lua, usually requires worlds to be set to the 'singlenode' mapgen.")
|
||||
# NOTE: tags: title for decorative
|
||||
pgettext("tags", "Decorative")
|
||||
# NOTE: tags: description for decorative
|
||||
pgettext("tags", "Adds nodes with no other purpose than for use in building")
|
||||
# NOTE: tags: title for developer_tools
|
||||
pgettext("tags", "Developer Tools")
|
||||
# NOTE: tags: description for developer_tools
|
||||
pgettext("tags", "Tools for game and mod developers")
|
||||
# NOTE: tags: title for education
|
||||
pgettext("tags", "Education")
|
||||
# NOTE: tags: description for education
|
||||
pgettext("tags", "Either has educational value, or is a tool to help teachers ")
|
||||
# NOTE: tags: title for environment
|
||||
pgettext("tags", "Environment / Weather")
|
||||
# NOTE: tags: description for environment
|
||||
pgettext("tags", "Improves the world, adding weather, ambient sounds, or other environment mechanics")
|
||||
# NOTE: tags: title for food
|
||||
pgettext("tags", "Food / Drinks")
|
||||
# NOTE: tags: title for gui
|
||||
pgettext("tags", "GUI")
|
||||
# NOTE: tags: description for gui
|
||||
pgettext("tags", "For content whose main utility or features are provided within a GUI, on-screen menu, or similar")
|
||||
# NOTE: tags: title for hud
|
||||
pgettext("tags", "HUD")
|
||||
# NOTE: tags: description for hud
|
||||
pgettext("tags", "For mods that grant the player extra information in the HUD")
|
||||
# NOTE: tags: title for inventory
|
||||
pgettext("tags", "Inventory")
|
||||
# NOTE: tags: description for inventory
|
||||
pgettext("tags", "Changes the inventory GUI")
|
||||
# NOTE: tags: title for jam_combat_mod
|
||||
pgettext("tags", "Jam / Combat 2020")
|
||||
# NOTE: tags: description for jam_combat_mod
|
||||
pgettext("tags", "For mods created for the Discord \"Combat\" modding event in 2020")
|
||||
# NOTE: tags: title for jam_game_2021
|
||||
pgettext("tags", "Jam / Game 2021")
|
||||
# NOTE: tags: description for jam_game_2021
|
||||
pgettext("tags", "Entries to the 2021 Minetest Game Jam")
|
||||
# NOTE: tags: title for jam_game_2022
|
||||
pgettext("tags", " Jam / Game 2022")
|
||||
# NOTE: tags: description for jam_game_2022
|
||||
pgettext("tags", "Entries to the 2022 Minetest Game Jam ")
|
||||
# NOTE: content_warnings: title for gore
|
||||
pgettext("content_warnings", "Gore")
|
||||
# NOTE: content_warnings: description for gore
|
||||
pgettext("content_warnings", "Blood, etc")
|
||||
# NOTE: content_warnings: title for gambling
|
||||
pgettext("content_warnings", "Gambling")
|
||||
# NOTE: content_warnings: description for gambling
|
||||
pgettext("content_warnings", "Games of chance, gambling games, etc")
|
||||
# NOTE: content_warnings: title for violence
|
||||
pgettext("content_warnings", "Violence")
|
||||
# NOTE: content_warnings: description for violence
|
||||
pgettext("content_warnings", "Non-cartoon violence. May be towards fantasy or human-like characters")
|
||||
# NOTE: content_warnings: title for horror
|
||||
pgettext("content_warnings", "Fear / Horror")
|
||||
# NOTE: content_warnings: description for horror
|
||||
pgettext("content_warnings", "Shocking and scary content. May scare young children")
|
||||
# NOTE: content_warnings: title for bad_language
|
||||
pgettext("content_warnings", "Bad Language")
|
||||
# NOTE: content_warnings: description for bad_language
|
||||
pgettext("content_warnings", "Contains swearing")
|
||||
# NOTE: tags: title for jam_game_2023
|
||||
pgettext("tags", "Jam / Game 2023")
|
||||
# NOTE: tags: description for jam_game_2023
|
||||
pgettext("tags", "Entries to the 2023 Minetest Game Jam ")
|
||||
# NOTE: tags: title for jam_game_2024
|
||||
pgettext("tags", "Jam / Game 2024")
|
||||
# NOTE: tags: description for jam_game_2024
|
||||
pgettext("tags", "Entries to the 2024 Luanti Game Jam")
|
||||
# NOTE: tags: title for jam_weekly_2021
|
||||
pgettext("tags", "Jam / Weekly Challenges 2021")
|
||||
# NOTE: tags: description for jam_weekly_2021
|
||||
pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding event in 2021")
|
||||
# NOTE: tags: title for less_than_px
|
||||
pgettext("tags", "<16px")
|
||||
# NOTE: tags: description for less_than_px
|
||||
pgettext("tags", "For less than 16px texture packs ")
|
||||
# NOTE: tags: title for library
|
||||
pgettext("tags", "API / Library")
|
||||
# NOTE: tags: description for library
|
||||
pgettext("tags", "Primarily adds an API for other mods to use")
|
||||
# NOTE: tags: title for magic
|
||||
pgettext("tags", "Magic / Enchanting")
|
||||
# NOTE: tags: title for mapgen
|
||||
pgettext("tags", "Mapgen / Biomes / Decoration")
|
||||
# NOTE: tags: description for mapgen
|
||||
pgettext("tags", "New mapgen or changes mapgen")
|
||||
# NOTE: tags: title for mini-game
|
||||
pgettext("tags", "Mini-game")
|
||||
# NOTE: tags: description for mini-game
|
||||
pgettext("tags", "Adds a mini-game to be played within Luanti")
|
||||
# NOTE: tags: title for mobs
|
||||
pgettext("tags", "Mobs / Animals / NPCs")
|
||||
# NOTE: tags: description for mobs
|
||||
pgettext("tags", "Adds mobs, animals, and non-player characters")
|
||||
# NOTE: tags: title for mtg
|
||||
pgettext("tags", "Minetest Game improved")
|
||||
# NOTE: tags: description for mtg
|
||||
pgettext("tags", "Forks of Minetest Game")
|
||||
# NOTE: tags: title for multiplayer
|
||||
pgettext("tags", "Multiplayer-focused")
|
||||
# NOTE: tags: description for multiplayer
|
||||
pgettext("tags", "Can/should only be used in multiplayer")
|
||||
# NOTE: tags: title for oneofakind__original
|
||||
pgettext("tags", "One-of-a-kind / Original")
|
||||
# NOTE: tags: description for oneofakind__original
|
||||
pgettext("tags", "For games and such that are of their own kind, distinct and original in nature to others of the same category.")
|
||||
# NOTE: tags: title for plants_and_farming
|
||||
pgettext("tags", "Plants and Farming")
|
||||
# NOTE: tags: description for plants_and_farming
|
||||
pgettext("tags", "Adds new plants or other farmable resources.")
|
||||
# NOTE: tags: title for player_effects
|
||||
pgettext("tags", "Player Effects / Power Ups")
|
||||
# NOTE: tags: description for player_effects
|
||||
pgettext("tags", "For content that changes player effects, including physics, for example: speed, jump height or gravity.")
|
||||
# NOTE: tags: title for puzzle
|
||||
pgettext("tags", "Puzzle")
|
||||
# NOTE: tags: description for puzzle
|
||||
pgettext("tags", "Focus on puzzle solving instead of combat")
|
||||
# NOTE: tags: title for pve
|
||||
pgettext("tags", "Player vs Environment (PvE)")
|
||||
# NOTE: tags: description for pve
|
||||
pgettext("tags", "For content designed for one or more players that focus on combat against the world, mobs, or NPCs.")
|
||||
# NOTE: tags: title for pvp
|
||||
pgettext("tags", "Player vs Player (PvP)")
|
||||
# NOTE: tags: description for pvp
|
||||
pgettext("tags", "Designed to be played competitively against other players")
|
||||
# NOTE: tags: title for seasonal
|
||||
pgettext("tags", "Seasonal")
|
||||
# NOTE: tags: description for seasonal
|
||||
pgettext("tags", "For content generally themed around a certain season or holiday")
|
||||
# NOTE: tags: title for server_tools
|
||||
pgettext("tags", "Server Moderation and Tools")
|
||||
# NOTE: tags: description for server_tools
|
||||
pgettext("tags", "Helps with server maintenance and moderation")
|
||||
# NOTE: tags: title for shooter
|
||||
pgettext("tags", "Shooter")
|
||||
# NOTE: tags: description for shooter
|
||||
pgettext("tags", "First person shooters (FPS) and more")
|
||||
# NOTE: tags: title for simulation
|
||||
pgettext("tags", "Sims")
|
||||
# NOTE: tags: description for simulation
|
||||
pgettext("tags", "Mods and games that aim to simulate real life activity. Similar to SimCity/The Sims/OpenTTD/etc.")
|
||||
# NOTE: tags: title for singleplayer
|
||||
pgettext("tags", "Singleplayer-focused")
|
||||
# NOTE: tags: description for singleplayer
|
||||
pgettext("tags", "Content that can be played alone")
|
||||
# NOTE: tags: title for skins
|
||||
pgettext("tags", "Player customization / Skins")
|
||||
# NOTE: tags: description for skins
|
||||
pgettext("tags", "Allows the player to customize their character by changing the texture or adding accessories.")
|
||||
# NOTE: tags: title for sound_music
|
||||
pgettext("tags", "Sounds / Music")
|
||||
# NOTE: tags: description for sound_music
|
||||
pgettext("tags", "Focuses on or adds new sounds or musical things")
|
||||
# NOTE: tags: title for sports
|
||||
pgettext("tags", "Sports")
|
||||
# NOTE: tags: title for storage
|
||||
pgettext("tags", "Storage")
|
||||
# NOTE: tags: description for storage
|
||||
pgettext("tags", "Adds or improves item storage mechanics")
|
||||
# NOTE: tags: title for strategy_rts
|
||||
pgettext("tags", "Strategy / RTS")
|
||||
# NOTE: tags: description for strategy_rts
|
||||
pgettext("tags", "Games and mods with a heavy strategy component, whether real-time or turn-based")
|
||||
# NOTE: tags: title for survival
|
||||
pgettext("tags", "Survival")
|
||||
# NOTE: tags: description for survival
|
||||
pgettext("tags", "Written specifically for survival gameplay with a focus on game-balance, difficulty level, or resources available through crafting, mining, ...")
|
||||
# NOTE: tags: title for technology
|
||||
pgettext("tags", "Machines / Electronics")
|
||||
# NOTE: tags: description for technology
|
||||
pgettext("tags", "Adds machines useful in automation, tubes, or power.")
|
||||
# NOTE: tags: title for tools
|
||||
pgettext("tags", "Tools / Weapons / Armor")
|
||||
# NOTE: tags: description for tools
|
||||
pgettext("tags", "Adds or changes tools, weapons, and armor")
|
||||
# NOTE: tags: title for transport
|
||||
pgettext("tags", "Transport")
|
||||
# NOTE: tags: description for transport
|
||||
pgettext("tags", "Adds or changes transportation methods. Includes teleportation, vehicles, ridable mobs, transport infrastructure and thematic content")
|
||||
# NOTE: tags: title for world_tools
|
||||
pgettext("tags", "World Maintenance and Tools")
|
||||
# NOTE: tags: description for world_tools
|
||||
pgettext("tags", "Tools to manage the world")
|
||||
# NOTE: content_warnings: title for alcohol_tobacco
|
||||
pgettext("content_warnings", "Alcohol / Tobacco")
|
||||
# NOTE: content_warnings: description for alcohol_tobacco
|
||||
pgettext("content_warnings", "Contains alcohol and/or tobacco")
|
||||
# NOTE: content_warnings: title for bad_language
|
||||
pgettext("content_warnings", "Bad Language")
|
||||
# NOTE: content_warnings: description for bad_language
|
||||
pgettext("content_warnings", "Contains swearing")
|
||||
# NOTE: content_warnings: title for drugs
|
||||
pgettext("content_warnings", "Drugs")
|
||||
# NOTE: content_warnings: description for drugs
|
||||
pgettext("content_warnings", "Contains recreational drugs other than alcohol or tobacco")
|
||||
# NOTE: content_warnings: title for gambling
|
||||
pgettext("content_warnings", "Gambling")
|
||||
# NOTE: content_warnings: description for gambling
|
||||
pgettext("content_warnings", "Games of chance, gambling games, etc")
|
||||
# NOTE: content_warnings: title for gore
|
||||
pgettext("content_warnings", "Gore")
|
||||
# NOTE: content_warnings: description for gore
|
||||
pgettext("content_warnings", "Blood, etc")
|
||||
# NOTE: content_warnings: title for horror
|
||||
pgettext("content_warnings", "Fear / Horror")
|
||||
# NOTE: content_warnings: description for horror
|
||||
pgettext("content_warnings", "Shocking and scary content. May scare young children")
|
||||
# NOTE: content_warnings: title for violence
|
||||
pgettext("content_warnings", "Violence")
|
||||
# NOTE: content_warnings: description for violence
|
||||
pgettext("content_warnings", "Non-cartoon violence. May be towards fantasy or human-like characters")
|
||||
|
||||
@@ -20,16 +20,16 @@ from typing import List
|
||||
import requests
|
||||
from celery import group, uuid
|
||||
from flask import redirect, url_for, flash, current_app
|
||||
from sqlalchemy import or_, and_
|
||||
from sqlalchemy import or_, and_, not_, func
|
||||
|
||||
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry
|
||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry, ReportAttachment
|
||||
from app.tasks.emails import send_pending_digests
|
||||
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
|
||||
from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
|
||||
import_languages, check_all_zip_files
|
||||
from app.tasks.usertasks import import_github_user_ids
|
||||
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links
|
||||
from app.tasks.usertasks import import_github_user_ids, do_delete_likely_spammers
|
||||
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links, update_file_size_bytes
|
||||
from app.utils import add_notification, get_system_user
|
||||
|
||||
actions = {}
|
||||
@@ -68,9 +68,10 @@ def clean_uploads():
|
||||
|
||||
release_urls = get_filenames_from_column(PackageRelease.url)
|
||||
screenshot_urls = get_filenames_from_column(PackageScreenshot.url)
|
||||
attachment_urls = get_filenames_from_column(ReportAttachment.url)
|
||||
pp_urls = get_filenames_from_column(User.profile_pic)
|
||||
|
||||
db_urls = release_urls.union(screenshot_urls).union(pp_urls)
|
||||
db_urls = release_urls.union(screenshot_urls).union(pp_urls).union(attachment_urls)
|
||||
unreachable = existing_uploads.difference(db_urls)
|
||||
|
||||
import sys
|
||||
@@ -322,6 +323,13 @@ def do_check_all_zip_files():
|
||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
||||
|
||||
|
||||
@action("Update file_size_bytes")
|
||||
def do_update_file_size_bytes():
|
||||
task_id = uuid()
|
||||
update_file_size_bytes.apply_async((), task_id=task_id)
|
||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
||||
|
||||
|
||||
@action("DANGER: Delete less popular removed packages")
|
||||
def del_less_popular_removed_packages():
|
||||
task_id = uuid()
|
||||
@@ -417,3 +425,10 @@ def delete_empty_threads():
|
||||
def check_for_broken_links():
|
||||
for package in Package.query.filter_by(state=PackageState.APPROVED).all():
|
||||
check_package_for_broken_links.delay(package.id)
|
||||
|
||||
|
||||
@action("DANGER: Delete likely spammers")
|
||||
def delete_likely_spammers():
|
||||
task_id = uuid()
|
||||
do_delete_likely_spammers.apply_async((), task_id=task_id)
|
||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
||||
|
||||
@@ -21,9 +21,10 @@ from wtforms import StringField, SubmitField, BooleanField
|
||||
from wtforms.validators import InputRequired, Length, Optional
|
||||
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
|
||||
get_int_or_abort
|
||||
from sqlalchemy import func
|
||||
from . import bp
|
||||
from .actions import actions
|
||||
from app.models import UserRank, Package, db, PackageState, User, AuditSeverity, NotificationType, PackageAlias
|
||||
from app.models import UserRank, Package, db, PackageState, PackageRelease, PackageScreenshot, User, AuditSeverity, NotificationType, PackageAlias
|
||||
from ...querybuilder import QueryBuilder
|
||||
|
||||
|
||||
@@ -182,6 +183,17 @@ def transfer():
|
||||
return render_template("admin/transfer.html", form=form)
|
||||
|
||||
|
||||
def sum_file_sizes(clazz):
|
||||
ret = {}
|
||||
for entry in (db.session
|
||||
.query(clazz.package_id, func.sum(clazz.file_size_bytes))
|
||||
.select_from(clazz)
|
||||
.group_by(clazz.package_id)
|
||||
.all()):
|
||||
ret[entry[0]] = entry[1]
|
||||
return ret
|
||||
|
||||
|
||||
@bp.route("/admin/storage/")
|
||||
@rank_required(UserRank.EDITOR)
|
||||
def storage():
|
||||
@@ -192,15 +204,20 @@ def storage():
|
||||
show_all = len(packages) < 100
|
||||
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50)
|
||||
|
||||
package_size_releases = sum_file_sizes(PackageRelease)
|
||||
package_size_screenshots = sum_file_sizes(PackageScreenshot)
|
||||
|
||||
data = []
|
||||
for package in packages:
|
||||
size_releases = sum([x.file_size_bytes for x in package.releases])
|
||||
size_screenshots = sum([x.file_size_bytes for x in package.screenshots])
|
||||
size_releases = package_size_releases.get(package.id, 0)
|
||||
size_screenshots = package_size_screenshots.get(package.id, 0)
|
||||
size_total = size_releases + size_screenshots
|
||||
if size_total < min_size * 1024 * 1024:
|
||||
continue
|
||||
|
||||
latest_release = package.releases.first()
|
||||
size_latest = latest_release.file_size_bytes if latest_release else 0
|
||||
size_total = size_releases + size_screenshots
|
||||
if size_total > min_size*1024*1024:
|
||||
data.append([package, size_total, size_releases, size_screenshots, size_latest])
|
||||
data.append([package, size_total, size_releases, size_screenshots, size_latest])
|
||||
|
||||
data.sort(key=lambda x: x[1], reverse=True)
|
||||
return render_template("admin/storage.html", data=data)
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_babel import lazy_gettext
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.validators import Optional, Length
|
||||
|
||||
from app.models import db, AuditLogEntry, UserRank, User, Permission
|
||||
from app.utils import rank_required, get_int_or_abort
|
||||
@@ -23,26 +27,40 @@ from app.utils import rank_required, get_int_or_abort
|
||||
from . import bp
|
||||
|
||||
|
||||
class AuditForm(FlaskForm):
|
||||
username = StringField(lazy_gettext("Username"), [Optional(), Length(0, 25)])
|
||||
q = StringField(lazy_gettext("Query"), [Optional(), Length(0, 300)])
|
||||
url = StringField(lazy_gettext("URL"), [Optional(), Length(0, 300)])
|
||||
submit = SubmitField(lazy_gettext("Search"), name=None)
|
||||
|
||||
|
||||
@bp.route("/admin/audit/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
@rank_required(UserRank.APPROVER)
|
||||
def audit():
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||
|
||||
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
|
||||
|
||||
if "username" in request.args:
|
||||
user = User.query.filter_by(username=request.args.get("username")).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
form = AuditForm(request.args)
|
||||
username = form.username.data
|
||||
q = form.q.data
|
||||
url = form.url.data
|
||||
if username:
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
query = query.filter_by(causer=user)
|
||||
|
||||
if "q" in request.args:
|
||||
q = request.args["q"]
|
||||
if q:
|
||||
query = query.filter(AuditLogEntry.title.ilike(f"%{q}%"))
|
||||
|
||||
if url:
|
||||
query = query.filter(AuditLogEntry.url.ilike(f"%{url}%"))
|
||||
|
||||
if not current_user.rank.at_least(UserRank.MODERATOR):
|
||||
query = query.filter(AuditLogEntry.package)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=num)
|
||||
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
|
||||
return render_template("admin/audit.html", log=pagination.items, pagination=pagination, form=form)
|
||||
|
||||
|
||||
@bp.route("/admin/audit/<int:id_>/")
|
||||
|
||||
@@ -23,14 +23,14 @@ from wtforms.validators import InputRequired, Length
|
||||
|
||||
from app.utils import rank_required, add_audit_log
|
||||
from . import bp
|
||||
from app.models import UserRank, MinetestRelease, db, AuditSeverity
|
||||
from app.models import UserRank, LuantiRelease, db, AuditSeverity
|
||||
|
||||
|
||||
@bp.route("/versions/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def version_list():
|
||||
return render_template("admin/versions/list.html",
|
||||
versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
|
||||
versions=LuantiRelease.query.order_by(db.asc(LuantiRelease.id)).all())
|
||||
|
||||
|
||||
class VersionForm(FlaskForm):
|
||||
@@ -45,14 +45,14 @@ class VersionForm(FlaskForm):
|
||||
def create_edit_version(name=None):
|
||||
version = None
|
||||
if name is not None:
|
||||
version = MinetestRelease.query.filter_by(name=name).first()
|
||||
version = LuantiRelease.query.filter_by(name=name).first()
|
||||
if version is None:
|
||||
abort(404)
|
||||
|
||||
form = VersionForm(formdata=request.form, obj=version)
|
||||
if form.validate_on_submit():
|
||||
if version is None:
|
||||
version = MinetestRelease(form.name.data)
|
||||
version = LuantiRelease(form.name.data)
|
||||
db.session.add(version)
|
||||
flash("Created version " + form.name.data, "success")
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ from app import csrf
|
||||
from app.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats
|
||||
from app.markdown import render_markdown
|
||||
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \
|
||||
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
|
||||
LuantiRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
|
||||
PackageAlias, Language
|
||||
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, cached, \
|
||||
cors_allowed
|
||||
from app.utils.minetest_hypertext import html_to_minetest, package_info_as_hypertext, package_reviews_as_hypertext
|
||||
from app.utils.luanti_hypertext import html_to_luanti, package_info_as_hypertext, package_reviews_as_hypertext
|
||||
from . import bp
|
||||
from .auth import is_api_authd
|
||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
||||
@@ -102,7 +102,7 @@ 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))
|
||||
version = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
|
||||
else:
|
||||
version = None
|
||||
|
||||
@@ -113,9 +113,10 @@ def package_view_client(package: Package):
|
||||
|
||||
formspec_version = get_int_or_abort(request.args["formspec_version"])
|
||||
include_images = is_yes(request.args.get("include_images", "true"))
|
||||
html = render_markdown(data["long_description"])
|
||||
page_url = package.get_url("packages.view", absolute=True)
|
||||
data["long_description"] = html_to_minetest(html, page_url, formspec_version, include_images)
|
||||
if data["long_description"] is not None:
|
||||
html = render_markdown(data["long_description"])
|
||||
data["long_description"] = html_to_luanti(html, page_url, formspec_version, include_images)
|
||||
|
||||
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
|
||||
|
||||
@@ -150,9 +151,9 @@ def package_view_client_reviews(package: Package):
|
||||
def package_hypertext(package):
|
||||
formspec_version = get_int_or_abort(request.args["formspec_version"])
|
||||
include_images = is_yes(request.args.get("include_images", "true"))
|
||||
html = render_markdown(package.desc)
|
||||
html = render_markdown(package.desc if package.desc else "")
|
||||
page_url = package.get_url("packages.view", absolute=True)
|
||||
return jsonify(html_to_minetest(html, page_url, formspec_version, include_images))
|
||||
return jsonify(html_to_luanti(html, page_url, formspec_version, include_images))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
|
||||
@@ -569,14 +570,14 @@ def package_scores():
|
||||
@cors_allowed
|
||||
@cached(60*60)
|
||||
def tags():
|
||||
return jsonify([tag.as_dict() for tag in Tag.query.all() ])
|
||||
return jsonify([tag.as_dict() for tag in Tag.query.order_by(db.asc(Tag.name)).all()])
|
||||
|
||||
|
||||
@bp.route("/api/content_warnings/")
|
||||
@cors_allowed
|
||||
@cached(60*60)
|
||||
def content_warnings():
|
||||
return jsonify([warning.as_dict() for warning in ContentWarning.query.all() ])
|
||||
return jsonify([warning.as_dict() for warning in ContentWarning.query.order_by(db.asc(ContentWarning.name)).all() ])
|
||||
|
||||
|
||||
@bp.route("/api/licenses/")
|
||||
@@ -629,38 +630,20 @@ def homepage():
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/welcome/v1/")
|
||||
@cors_allowed
|
||||
def welcome_v1():
|
||||
featured = Package.query \
|
||||
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
|
||||
Package.collections.any(
|
||||
and_(Collection.name == "featured", Collection.author.has(username="ContentDB")))) \
|
||||
.order_by(func.random()) \
|
||||
.limit(5).all()
|
||||
|
||||
def map_packages(packages: List[Package]):
|
||||
return [pkg.as_short_dict(current_app.config["BASE_URL"]) for pkg in packages]
|
||||
|
||||
return jsonify({
|
||||
"featured": map_packages(featured),
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/minetest_versions/")
|
||||
@cors_allowed
|
||||
def versions():
|
||||
protocol_version = request.args.get("protocol_version")
|
||||
engine_version = request.args.get("engine_version")
|
||||
if protocol_version or engine_version:
|
||||
rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
|
||||
rel = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
|
||||
if rel is None:
|
||||
error(404, "No releases found")
|
||||
|
||||
return jsonify(rel.as_dict())
|
||||
|
||||
return jsonify([rel.as_dict() \
|
||||
for rel in MinetestRelease.query.all() if rel.get_actual() is not None])
|
||||
for rel in LuantiRelease.query.all() if rel.get_actual() is not None])
|
||||
|
||||
|
||||
@bp.route("/api/languages/")
|
||||
@@ -852,7 +835,7 @@ def hypertext():
|
||||
if request.content_type == "text/markdown":
|
||||
html = render_markdown(html)
|
||||
|
||||
return jsonify(html_to_minetest(html, "", formspec_version, include_images))
|
||||
return jsonify(html_to_luanti(html, "", formspec_version, include_images))
|
||||
|
||||
|
||||
@bp.route("/api/collections/")
|
||||
@@ -903,9 +886,9 @@ def collection_view(token, author, name):
|
||||
@cached(300)
|
||||
def updates():
|
||||
protocol_version = get_int_or_abort(request.args.get("protocol_version"))
|
||||
minetest_version = request.args.get("engine_version")
|
||||
if protocol_version or minetest_version:
|
||||
version = MinetestRelease.get(minetest_version, protocol_version)
|
||||
engine_version = request.args.get("engine_version")
|
||||
if protocol_version or engine_version:
|
||||
version = LuantiRelease.get(engine_version, protocol_version)
|
||||
else:
|
||||
version = None
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
|
||||
from app.logic.packages import do_edit_package
|
||||
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
|
||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
|
||||
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
|
||||
from app.models import APIToken, Package, LuantiRelease, PackageScreenshot
|
||||
|
||||
|
||||
def error(code: int, msg: str):
|
||||
@@ -39,7 +39,7 @@ def guard(f):
|
||||
|
||||
|
||||
def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
|
||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API"):
|
||||
if not token.can_operate_on_package(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
@@ -55,7 +55,7 @@ def api_create_vcs_release(token: APIToken, package: Package, name: str, title:
|
||||
|
||||
|
||||
def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API", commit_hash: str = None):
|
||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API", commit_hash: str = None):
|
||||
if not token.can_operate_on_package(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
@@ -112,9 +112,9 @@ def api_edit_package(token: APIToken, package: Package, data: dict, reason: str
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
package = guard(do_edit_package)(token.owner, package, False, False, data, reason)
|
||||
|
||||
was_modified = guard(do_edit_package)(token.owner, package, False, False, data, reason)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"package": package.as_dict(current_app.config["BASE_URL"])
|
||||
"package": package.as_dict(current_app.config["BASE_URL"]),
|
||||
"was_modified": was_modified,
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for
|
||||
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for, jsonify
|
||||
from flask_babel import lazy_gettext, gettext
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
@@ -25,7 +25,7 @@ from wtforms import StringField, BooleanField, SubmitField, FieldList, HiddenFie
|
||||
from wtforms.validators import InputRequired, Length, Optional, Regexp
|
||||
|
||||
from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity
|
||||
from app.utils import nonempty_or_none, normalize_line_endings
|
||||
from app.utils import nonempty_or_none, normalize_line_endings, should_return_json
|
||||
from app.utils.models import is_package_page, add_audit_log, create_session
|
||||
|
||||
bp = Blueprint("collections", __name__)
|
||||
@@ -70,7 +70,10 @@ def view(author, name):
|
||||
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
|
||||
items = [x for x in items if x.package.check_perm(current_user, Permission.VIEW_PACKAGE)]
|
||||
|
||||
return render_template("collections/view.html", collection=collection, items=items)
|
||||
if should_return_json():
|
||||
return jsonify([ item.package.as_key_dict() for item in items ])
|
||||
else:
|
||||
return render_template("collections/view.html", collection=collection, items=items)
|
||||
|
||||
|
||||
class CollectionForm(FlaskForm):
|
||||
|
||||
@@ -29,10 +29,10 @@ def _make_feed(title: str, feed_url: str, items: list):
|
||||
return {
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": title,
|
||||
"description": gettext("Welcome to the best place to find Minetest mods, games, and texture packs"),
|
||||
"home_page_url": "https://content.minetest.net/",
|
||||
"description": gettext("Welcome to the best place to find Luanti mods, games, and texture packs"),
|
||||
"home_page_url": "https://content.luanti.org/",
|
||||
"feed_url": feed_url,
|
||||
"icon": "https://content.minetest.net/favicon-128.png",
|
||||
"icon": "https://content.luanti.org/favicon-128.png",
|
||||
"expired": False,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ from sqlalchemy.sql.expression import func
|
||||
|
||||
PKGS_PER_ROW = 4
|
||||
|
||||
# GAMEJAM_BANNER = "https://jam.minetest.net/img/banner.png"
|
||||
# GAMEJAM_BANNER = "https://jam.luanti.org/img/banner.png"
|
||||
#
|
||||
# class GameJam:
|
||||
# cover_image = type("", (), dict(url=GAMEJAM_BANNER))()
|
||||
@@ -40,7 +40,7 @@ PKGS_PER_ROW = 4
|
||||
# def get_url(self, _name):
|
||||
# return "/gamejam/"
|
||||
#
|
||||
# title = "Minetest Game Jam 2023: \"Unexpected\""
|
||||
# title = "Luanti Game Jam 2023: \"Unexpected\""
|
||||
# author = None
|
||||
#
|
||||
# short_desc = "The game jam has finished! It's now up to the community to play and rate the games."
|
||||
@@ -51,7 +51,7 @@ PKGS_PER_ROW = 4
|
||||
|
||||
@bp.route("/gamejam/")
|
||||
def gamejam():
|
||||
return redirect("https://jam.minetest.net/")
|
||||
return redirect("https://jam.luanti.org/")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
|
||||
@@ -194,6 +194,10 @@ def create_edit_client(username, id_=None):
|
||||
|
||||
if form.validate_on_submit():
|
||||
if is_new:
|
||||
if OAuthClient.query.filter(OAuthClient.title.ilike(form.title.data.strip())).count() > 0:
|
||||
flash(gettext("An OAuth client with that title already exists. Please choose a new title."), "danger")
|
||||
return render_template("oauth/create_edit.html", user=user, form=form, client=client)
|
||||
|
||||
client = OAuthClient()
|
||||
db.session.add(client)
|
||||
client.owner = user
|
||||
@@ -201,6 +205,7 @@ def create_edit_client(username, id_=None):
|
||||
client.secret = random_string(32)
|
||||
client.approved = current_user.rank.at_least(UserRank.EDITOR)
|
||||
|
||||
|
||||
form.populate_obj(client)
|
||||
|
||||
verb = "Created" if is_new else "Edited"
|
||||
|
||||
@@ -23,7 +23,7 @@ from wtforms.validators import Optional
|
||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
|
||||
|
||||
from . import bp
|
||||
from ...models import PackageType, Tag, db, ContentWarning, License, Language, MinetestRelease, Package, PackageState
|
||||
from ...models import PackageType, Tag, db, ContentWarning, License, Language, LuantiRelease, Package, PackageState
|
||||
|
||||
|
||||
def make_label(obj: Tag | ContentWarning):
|
||||
@@ -74,8 +74,8 @@ class AdvancedSearchForm(FlaskForm):
|
||||
allow_blank=True, blank_value="",
|
||||
get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
|
||||
engine_version = QuerySelectField(lazy_gettext("Minetest Version"),
|
||||
query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)),
|
||||
engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
|
||||
query_factory=lambda: LuantiRelease.query.order_by(db.asc(LuantiRelease.id)),
|
||||
allow_blank=True, blank_value="",
|
||||
get_pk=lambda a: a.value, get_label=lambda a: a.name)
|
||||
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[
|
||||
|
||||
@@ -233,7 +233,7 @@ class PackageForm(FlaskForm):
|
||||
name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_) only"))])
|
||||
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
|
||||
|
||||
dev_state = SelectField(lazy_gettext("Maintenance State"), [InputRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
|
||||
dev_state = SelectField(lazy_gettext("Maintenance State"), [DataRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
|
||||
|
||||
tags = QuerySelectMultipleField(lazy_gettext('Tags'), query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=make_label)
|
||||
content_warnings = QuerySelectMultipleField(lazy_gettext('Content Warnings'), query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=make_label)
|
||||
@@ -266,6 +266,7 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
|
||||
flash(
|
||||
gettext("Package already exists, but is removed. Please contact ContentDB staff to restore the package"),
|
||||
"danger")
|
||||
return redirect(url_for("report.report", url=package.get_url("packages.view")))
|
||||
else:
|
||||
flash(markupsafe.Markup(
|
||||
f"<a class='btn btn-sm btn-danger float-end' href='{package.get_url('packages.view')}'>View</a>" +
|
||||
@@ -305,10 +306,6 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
|
||||
"translation_url": form.translation_url.data,
|
||||
})
|
||||
|
||||
if wasNew:
|
||||
msg = f"Created package {author.username}/{form.name.data}"
|
||||
add_audit_log(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package)
|
||||
|
||||
if wasNew and package.repo is not None:
|
||||
import_repo_screenshot.delay(package.id)
|
||||
|
||||
@@ -321,13 +318,14 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
|
||||
return redirect(next_url)
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
@bp.route("/packages/new/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_edit(author=None, name=None):
|
||||
if current_user.email is None:
|
||||
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
|
||||
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
||||
return redirect(url_for("users.email_notifications"))
|
||||
|
||||
@@ -458,6 +456,7 @@ def move_to_state(package):
|
||||
@is_package_page
|
||||
def translation(package):
|
||||
return render_template("packages/translation.html", package=package,
|
||||
has_content_translations=any([x.title or x.short_desc for x in package.translations.all()]),
|
||||
tabs=get_package_tabs(current_user, package), current_tab="translation")
|
||||
|
||||
|
||||
@@ -570,7 +569,7 @@ def edit_maintainers(package):
|
||||
|
||||
for user in users:
|
||||
if not user in package.maintainers:
|
||||
if thread:
|
||||
if thread and user not in thread.watchers:
|
||||
thread.watchers.append(user)
|
||||
add_notification(user, current_user, NotificationType.MAINTAINER,
|
||||
"Added you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package)
|
||||
|
||||
@@ -25,7 +25,7 @@ from wtforms.validators import InputRequired, Length, Optional
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
|
||||
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, MinetestRelease, \
|
||||
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, LuantiRelease, \
|
||||
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
|
||||
from app.rediscache import has_key, set_temp_key, make_download_key
|
||||
from app.tasks.importtasks import check_update_config
|
||||
@@ -42,11 +42,11 @@ def list_releases(package):
|
||||
|
||||
|
||||
def get_mt_releases(is_max):
|
||||
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
|
||||
query = LuantiRelease.query.order_by(db.asc(LuantiRelease.id))
|
||||
if is_max:
|
||||
query = query.limit(query.count() - 1)
|
||||
else:
|
||||
query = query.filter(MinetestRelease.name != "0.4.17")
|
||||
query = query.filter(LuantiRelease.name != "0.4.17")
|
||||
|
||||
return query
|
||||
|
||||
@@ -59,9 +59,9 @@ class CreatePackageReleaseForm(FlaskForm):
|
||||
upload_mode = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload")
|
||||
vcs_label = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None)
|
||||
file_upload = FileField(lazy_gettext("File Upload"))
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
submit = SubmitField(lazy_gettext("Save"))
|
||||
|
||||
@@ -74,9 +74,9 @@ class EditPackageReleaseForm(FlaskForm):
|
||||
url = StringField(lazy_gettext("URL"), [Optional()])
|
||||
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
|
||||
approved = BooleanField(lazy_gettext("Is Approved"))
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
submit = SubmitField(lazy_gettext("Save"))
|
||||
|
||||
@@ -85,7 +85,7 @@ class EditPackageReleaseForm(FlaskForm):
|
||||
@login_required
|
||||
@is_package_page
|
||||
def create_release(package):
|
||||
if current_user.email is None:
|
||||
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
|
||||
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
||||
return redirect(url_for("users.email_notifications"))
|
||||
|
||||
@@ -127,9 +127,10 @@ def download_release(package, id):
|
||||
|
||||
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
if ip is not None and not is_user_bot():
|
||||
is_minetest = (request.headers.get("User-Agent") or "").startswith("Minetest")
|
||||
user_agent = request.headers.get("User-Agent") or ""
|
||||
is_luanti = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
|
||||
reason = request.args.get("reason")
|
||||
PackageDailyStats.update(package, is_minetest, reason)
|
||||
PackageDailyStats.update(package, is_luanti, reason)
|
||||
|
||||
key = make_download_key(ip, release.package)
|
||||
if not has_key(key):
|
||||
@@ -214,10 +215,10 @@ def edit_release(package, id):
|
||||
|
||||
class BulkReleaseForm(FlaskForm):
|
||||
set_min = BooleanField(lazy_gettext("Set Min"))
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
|
||||
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
set_max = BooleanField(lazy_gettext("Set Max"))
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
|
||||
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
|
||||
submit = SubmitField(lazy_gettext("Update"))
|
||||
|
||||
@@ -19,8 +19,9 @@ from flask import render_template, request, redirect, flash, url_for, abort
|
||||
from flask_babel import lazy_gettext, gettext
|
||||
from flask_login import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileRequired
|
||||
from wtforms import StringField, SubmitField, BooleanField, FileField
|
||||
from wtforms.validators import InputRequired, Length, DataRequired, Optional
|
||||
from wtforms.validators import Length, DataRequired, Optional
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
@@ -32,7 +33,7 @@ from app.utils import is_package_page
|
||||
|
||||
class CreateScreenshotForm(FlaskForm):
|
||||
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
|
||||
file_upload = FileField(lazy_gettext("File Upload"), [InputRequired()])
|
||||
file_upload = FileField(lazy_gettext("File Upload"), [FileRequired()])
|
||||
submit = SubmitField(lazy_gettext("Save"))
|
||||
|
||||
|
||||
|
||||
@@ -14,24 +14,30 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint, request, render_template, url_for, abort
|
||||
from flask import Blueprint, request, render_template, url_for, abort, flash
|
||||
from flask_babel import lazy_gettext
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from werkzeug.utils import redirect
|
||||
from wtforms import TextAreaField, SubmitField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
|
||||
from wtforms.validators import InputRequired, Length, Optional, DataRequired
|
||||
|
||||
from app.models import User, UserRank
|
||||
from app.tasks.emails import send_user_email
|
||||
from app.logic.uploads import upload_file
|
||||
from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
|
||||
from app.tasks.webhooktasks import post_discord_webhook
|
||||
from app.utils import is_no, abs_url_samesite, normalize_line_endings
|
||||
from app.utils import (is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for,
|
||||
random_string, add_replies)
|
||||
|
||||
bp = Blueprint("report", __name__)
|
||||
|
||||
|
||||
class ReportForm(FlaskForm):
|
||||
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)], filters=[normalize_line_endings])
|
||||
category = SelectField(lazy_gettext("Category"), [DataRequired()], choices=ReportCategory.choices(with_none=True), coerce=ReportCategory.coerce)
|
||||
|
||||
url = URLField(lazy_gettext("URL"), [Optional()])
|
||||
title = StringField(lazy_gettext("Subject / Title"), [InputRequired(), Length(10, 300)])
|
||||
message = TextAreaField(lazy_gettext("Message"), [Optional(), Length(0, 10000)], filters=[normalize_line_endings])
|
||||
file_upload = FileField(lazy_gettext("Image Upload"), [Optional()])
|
||||
submit = SubmitField(lazy_gettext("Report"))
|
||||
|
||||
|
||||
@@ -46,22 +52,131 @@ def report():
|
||||
|
||||
url = abs_url_samesite(url)
|
||||
|
||||
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
|
||||
form = ReportForm() if current_user.is_authenticated else None
|
||||
if form and request.method == "GET":
|
||||
try:
|
||||
form.category.data = ReportCategory.coerce(request.args.get("category"))
|
||||
except KeyError:
|
||||
pass
|
||||
form.url.data = url
|
||||
form.title.data = request.args.get("title", "")
|
||||
|
||||
if form and form.validate_on_submit():
|
||||
report = Report()
|
||||
report.id = random_string(8)
|
||||
report.user = current_user if current_user.is_authenticated else None
|
||||
form.populate_obj(report)
|
||||
|
||||
if current_user.is_authenticated:
|
||||
user_info = f"{current_user.username}"
|
||||
thread = Thread()
|
||||
thread.title = f"Report: {form.title.data}"
|
||||
thread.author = current_user
|
||||
thread.private = True
|
||||
thread.watchers.extend(User.query.filter(User.rank >= UserRank.MODERATOR).all())
|
||||
db.session.add(thread)
|
||||
db.session.flush()
|
||||
|
||||
report.thread = thread
|
||||
|
||||
add_replies(thread, current_user, f"**{report.category.title} report created**\n\n{form.message.data}")
|
||||
else:
|
||||
user_info = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
report.message = ip_addr + "\n\n" + report.message
|
||||
|
||||
text = f"{url}\n\n{form.message.data}"
|
||||
db.session.add(report)
|
||||
db.session.flush()
|
||||
|
||||
task = None
|
||||
for admin in User.query.filter_by(rank=UserRank.ADMIN).all():
|
||||
task = send_user_email.delay(admin.email, admin.locale or "en",
|
||||
f"User report from {user_info}", text)
|
||||
if form.file_upload.data:
|
||||
atmt = ReportAttachment()
|
||||
report.attachments.add(atmt)
|
||||
uploaded_url, _ = upload_file(form.file_upload.data, "image", lazy_gettext("a PNG, JPEG, or WebP image file"))
|
||||
atmt.url = uploaded_url
|
||||
db.session.add(atmt)
|
||||
|
||||
post_discord_webhook.delay(None if is_anon else current_user.username, f"**New Report**\n{url}\n\n{form.message.data}", True)
|
||||
if current_user.is_authenticated:
|
||||
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
|
||||
url_for("report.view", rid=report.id))
|
||||
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("homepage.home")))
|
||||
db.session.commit()
|
||||
|
||||
return render_template("report/index.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
||||
abs_url = abs_url_for("report.view", rid=report.id)
|
||||
msg = f"**New Report**\nReport on `{report.url}`\n\n{report.title}\n\nView: {abs_url}"
|
||||
post_discord_webhook.delay(None if is_anon else current_user.username, msg, True)
|
||||
|
||||
return redirect(url_for("report.report_received", rid=report.id))
|
||||
|
||||
return render_template("report/report.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
||||
|
||||
|
||||
@bp.route("/report/received/")
|
||||
def report_received():
|
||||
rid = request.args.get("rid")
|
||||
report = Report.query.get_or_404(rid)
|
||||
return render_template("report/report_received.html", report=report)
|
||||
|
||||
|
||||
@bp.route("/admin/reports/")
|
||||
@rank_required(UserRank.EDITOR)
|
||||
def list_all():
|
||||
reports = Report.query.order_by(db.asc(Report.is_resolved), db.asc(Report.created_at)).all()
|
||||
return render_template("report/list.html", reports=reports)
|
||||
|
||||
|
||||
@bp.route("/admin/reports/<rid>/", methods=["GET", "POST"])
|
||||
def view(rid: str):
|
||||
report = Report.query.get_or_404(rid)
|
||||
if not report.check_perm(current_user, Permission.SEE_REPORT):
|
||||
abort(404)
|
||||
|
||||
if request.method == "POST":
|
||||
if report.is_resolved:
|
||||
if "reopen" in request.form:
|
||||
report.is_resolved = False
|
||||
url = url_for("report.view", rid=report.id)
|
||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Reopened report \"{report.title}\"", url)
|
||||
|
||||
if report.thread:
|
||||
add_replies(report.thread, current_user, f"Reopened report", is_status_update=True)
|
||||
|
||||
db.session.commit()
|
||||
else:
|
||||
if "completed" in request.form:
|
||||
outcome = "as completed"
|
||||
elif "removed" in request.form:
|
||||
outcome = "as content removed"
|
||||
elif "invalid" in request.form:
|
||||
outcome = "without action"
|
||||
if report.thread:
|
||||
flash("Make sure to comment why the report is invalid in the thread", "warning")
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
report.is_resolved = True
|
||||
url = url_for("report.view", rid=report.id)
|
||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Report closed {outcome} \"{report.title}\"", url)
|
||||
|
||||
if report.thread:
|
||||
add_replies(report.thread, current_user, f"Closed report {outcome}", is_status_update=True)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return render_template("report/view.html", report=report)
|
||||
|
||||
|
||||
@bp.route("/admin/reports/<rid>/edit/", methods=["GET", "POST"])
|
||||
def edit(rid: str):
|
||||
report = Report.query.get_or_404(rid)
|
||||
if not report.check_perm(current_user, Permission.SEE_REPORT):
|
||||
abort(404)
|
||||
|
||||
form = ReportForm(request.form, obj=report)
|
||||
form.submit.label.text = lazy_gettext("Save")
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(report)
|
||||
url = url_for("report.view", rid=report.id)
|
||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited report \"{report.title}\"", url)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("report.view", rid=report.id))
|
||||
|
||||
return render_template("report/edit.html", report=report, form=form)
|
||||
|
||||
@@ -29,7 +29,7 @@ from app.models import Package, db, User, Permission, Thread, UserRank, AuditSev
|
||||
from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains, \
|
||||
normalize_line_endings
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, TextAreaField, SubmitField, BooleanField
|
||||
from wtforms import StringField, TextAreaField, SubmitField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
from app.utils import get_int_or_abort
|
||||
|
||||
@@ -254,6 +254,9 @@ def view(id):
|
||||
if mentioned is None:
|
||||
continue
|
||||
|
||||
if not thread.check_perm(mentioned, Permission.SEE_THREAD):
|
||||
continue
|
||||
|
||||
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
|
||||
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
|
||||
msg, thread.get_view_url(), thread.package)
|
||||
@@ -281,7 +284,6 @@ def view(id):
|
||||
class ThreadForm(FlaskForm):
|
||||
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)])
|
||||
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
|
||||
private = BooleanField(lazy_gettext("Private"))
|
||||
btn_submit = SubmitField(lazy_gettext("Open Thread"))
|
||||
|
||||
|
||||
@@ -296,14 +298,11 @@ def new():
|
||||
if package is None:
|
||||
abort(404)
|
||||
|
||||
def_is_private = request.args.get("private") or False
|
||||
if package is None and not current_user.rank.at_least(UserRank.APPROVER):
|
||||
abort(404)
|
||||
|
||||
is_review_thread = package and not package.approved
|
||||
allow_private_change = not is_review_thread
|
||||
if is_review_thread:
|
||||
def_is_private = True
|
||||
is_private_thread = is_review_thread
|
||||
|
||||
# Check that user can make the thread
|
||||
if package and not package.check_perm(current_user, Permission.CREATE_THREAD):
|
||||
@@ -326,7 +325,6 @@ def new():
|
||||
|
||||
# Set default values
|
||||
elif request.method == "GET":
|
||||
form.private.data = def_is_private
|
||||
form.title.data = request.args.get("title") or ""
|
||||
|
||||
# Validate and submit
|
||||
@@ -337,7 +335,7 @@ def new():
|
||||
thread = Thread()
|
||||
thread.author = current_user
|
||||
thread.title = form.title.data
|
||||
thread.private = form.private.data if allow_private_change else def_is_private
|
||||
thread.private = is_private_thread
|
||||
thread.package = package
|
||||
db.session.add(thread)
|
||||
|
||||
@@ -367,7 +365,8 @@ def new():
|
||||
add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
|
||||
msg, thread.get_view_url(), thread.package)
|
||||
|
||||
thread.watchers.append(mentioned)
|
||||
if mentioned not in thread.watchers:
|
||||
thread.watchers.append(mentioned)
|
||||
|
||||
notif_msg = "New thread '{}'".format(thread.title)
|
||||
if package is not None:
|
||||
@@ -384,7 +383,7 @@ def new():
|
||||
|
||||
return redirect(thread.get_view_url())
|
||||
|
||||
return render_template("threads/new.html", form=form, allow_private_change=allow_private_change, package=package)
|
||||
return render_template("threads/new.html", form=form, package=package)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/comments/")
|
||||
|
||||
@@ -25,7 +25,11 @@ bp = Blueprint("thumbnails", __name__)
|
||||
|
||||
|
||||
ALLOWED_RESOLUTIONS = [(100, 67), (270, 180), (350, 233), (1100, 520)]
|
||||
ALLOWED_EXTENSIONS = {"png", "webp", "jpg"}
|
||||
ALLOWED_MIMETYPES = {
|
||||
"png": "image/png",
|
||||
"webp": "image/webp",
|
||||
"jpg": "image/jpeg",
|
||||
}
|
||||
|
||||
|
||||
def mkdir(path):
|
||||
@@ -76,10 +80,10 @@ def find_source_file(img):
|
||||
period = source_filepath.rfind(".")
|
||||
start = source_filepath[:period]
|
||||
ext = source_filepath[period + 1:]
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
if ext not in ALLOWED_MIMETYPES:
|
||||
abort(404)
|
||||
|
||||
for other_ext in ALLOWED_EXTENSIONS:
|
||||
for other_ext in ALLOWED_MIMETYPES.keys():
|
||||
other_path = f"{start}.{other_ext}"
|
||||
if ext != other_ext and os.path.isfile(other_path):
|
||||
return other_path
|
||||
@@ -87,6 +91,15 @@ def find_source_file(img):
|
||||
abort(404)
|
||||
|
||||
|
||||
def get_mimetype(cache_filepath: str) -> str:
|
||||
period = cache_filepath.rfind(".")
|
||||
ext = cache_filepath[period + 1:]
|
||||
mimetype = ALLOWED_MIMETYPES.get(ext)
|
||||
if mimetype is None:
|
||||
abort(404)
|
||||
return mimetype
|
||||
|
||||
|
||||
@bp.route("/thumbnails/<int:level>/<img>")
|
||||
def make_thumbnail(img, level):
|
||||
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
|
||||
@@ -104,7 +117,7 @@ def make_thumbnail(img, level):
|
||||
source_filepath = find_source_file(img)
|
||||
resize_and_crop(source_filepath, cache_filepath, (w, h))
|
||||
|
||||
res = send_file(cache_filepath)
|
||||
res = send_file(cache_filepath, mimetype=get_mimetype(cache_filepath))
|
||||
res.headers["Cache-Control"] = "max-age=604800" # 1 week
|
||||
return res
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from flask_login import current_user, login_required
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
|
||||
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, MinetestRelease
|
||||
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, LuantiRelease, Report
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.utils import get_int_or_abort, is_yes, rank_required
|
||||
from . import bp
|
||||
@@ -83,11 +83,13 @@ def view_editor():
|
||||
.order_by(db.desc(AuditLogEntry.created_at)) \
|
||||
.limit(20).all()
|
||||
|
||||
reports = Report.query.filter_by(is_resolved=False).order_by(db.asc(Report.created_at)).all() if current_user.rank.at_least(UserRank.EDITOR) else None
|
||||
|
||||
return render_template("todo/editor.html", current_tab="editor",
|
||||
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
|
||||
can_approve_new=can_approve_new, can_approve_rel=can_approve_rel, can_approve_scn=can_approve_scn,
|
||||
license_needed=license_needed, total_packages=total_packages, total_to_tag=total_to_tag,
|
||||
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log)
|
||||
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log, reports=reports)
|
||||
|
||||
|
||||
@bp.route("/todo/tags/")
|
||||
@@ -170,7 +172,7 @@ def screenshots():
|
||||
def mtver_support():
|
||||
is_mtm_only = is_yes(request.args.get("mtm"))
|
||||
|
||||
current_stable = MinetestRelease.query.filter(~MinetestRelease.name.like("%-dev")).order_by(db.desc(MinetestRelease.id)).first()
|
||||
current_stable = LuantiRelease.query.filter(~LuantiRelease.name.like("%-dev")).order_by(db.desc(LuantiRelease.id)).first()
|
||||
|
||||
query = db.session.query(Package) \
|
||||
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \
|
||||
|
||||
@@ -104,7 +104,7 @@ class RegisterForm(FlaskForm):
|
||||
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
|
||||
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
|
||||
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
|
||||
agree = BooleanField(lazy_gettext("I agree"), [DataRequired()])
|
||||
first_name = StringField("First name", [])
|
||||
submit = SubmitField(lazy_gettext("Register"))
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ def handle_register(form):
|
||||
return user
|
||||
elif user is None:
|
||||
return
|
||||
elif form.first_name.data != "":
|
||||
abort(500)
|
||||
|
||||
user.password = make_flask_login_password(form.password.data)
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def claim_forums():
|
||||
|
||||
# Get signature
|
||||
try:
|
||||
profile = get_profile("https://forum.minetest.net", username)
|
||||
profile = get_profile("https://forum.luanti.org", username)
|
||||
sig = profile.signature if profile else None
|
||||
except IOError as e:
|
||||
if hasattr(e, 'message'):
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.models import Package, APIToken, Permission, PackageState
|
||||
|
||||
|
||||
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
|
||||
repo_url = repo_url.replace("https://", "").replace("http://", "").lower()
|
||||
if token.package:
|
||||
packages = [token.package]
|
||||
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):
|
||||
|
||||
@@ -28,7 +28,6 @@ from app.blueprints.api.support import error, api_create_vcs_release
|
||||
from app.logic.users import create_user
|
||||
from app.models import db, User, APIToken, AuditSeverity
|
||||
from app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
|
||||
|
||||
from . import bp
|
||||
from .common import get_packages_for_vcs_and_token
|
||||
|
||||
@@ -98,6 +97,7 @@ def github_callback(oauth_token):
|
||||
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
|
||||
return redirect(url_for("users.login"))
|
||||
|
||||
user_by_github.github_username = github_username
|
||||
add_audit_log(AuditSeverity.USER, user_by_github, "Logged in using GitHub OAuth",
|
||||
url_for("users.profile", username=user_by_github.username))
|
||||
db.session.commit()
|
||||
|
||||
@@ -38,7 +38,7 @@ def webhook_impl():
|
||||
if token is None:
|
||||
return error(403, "Invalid authentication")
|
||||
|
||||
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"].replace("https://", "").replace("http://", ""))
|
||||
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"])
|
||||
for package in packages:
|
||||
#
|
||||
# Check event
|
||||
|
||||
@@ -14,27 +14,27 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from celery import uuid
|
||||
from flask import Blueprint, render_template, redirect, request, abort, url_for
|
||||
from flask_babel import lazy_gettext
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, BooleanField, SubmitField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
from wtforms import StringField, BooleanField, SubmitField, SelectMultipleField
|
||||
from wtforms.validators import InputRequired, Length, Optional
|
||||
|
||||
from app.tasks import celery
|
||||
from app.utils import rank_required
|
||||
|
||||
bp = Blueprint("zipgrep", __name__)
|
||||
|
||||
from app.models import UserRank, Package
|
||||
from app.models import UserRank, Package, PackageType
|
||||
from app.tasks.zipgrep import search_in_releases
|
||||
|
||||
|
||||
class SearchForm(FlaskForm):
|
||||
query = StringField(lazy_gettext("Text to find (regex)"), [InputRequired(), Length(1, 100)])
|
||||
file_filter = StringField(lazy_gettext("File filter"), [InputRequired(), Length(1, 100)], default="*.lua")
|
||||
remember_me = BooleanField(lazy_gettext("Remember me"), default=True)
|
||||
type = SelectMultipleField(lazy_gettext("Type"), [Optional()],
|
||||
choices=PackageType.choices(), coerce=PackageType.coerce)
|
||||
submit = SubmitField(lazy_gettext("Search"))
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def zipgrep_search():
|
||||
form = SearchForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
task_id = uuid()
|
||||
search_in_releases.apply_async((form.query.data, form.file_filter.data), task_id=task_id)
|
||||
search_in_releases.apply_async((form.query.data, form.file_filter.data, [x.name for x in form.type.data]), task_id=task_id)
|
||||
result_url = url_for("zipgrep.view_results", id=task_id)
|
||||
return redirect(url_for("tasks.check", id=task_id, r=result_url))
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import datetime
|
||||
|
||||
from .models import User, UserRank, MinetestRelease, Tag, License, Notification, NotificationType, Package, \
|
||||
from .models import User, UserRank, LuantiRelease, Tag, License, Notification, NotificationType, Package, \
|
||||
PackageState, PackageType, PackageRelease, MetaPackage, Dependency
|
||||
from .utils import make_flask_login_password
|
||||
|
||||
@@ -35,12 +35,12 @@ def populate(session):
|
||||
system_user.rank = UserRank.BOT
|
||||
session.add(system_user)
|
||||
|
||||
session.add(MinetestRelease("None", 0))
|
||||
session.add(MinetestRelease("0.4.16/17", 32))
|
||||
session.add(MinetestRelease("5.0", 37))
|
||||
session.add(MinetestRelease("5.1", 38))
|
||||
session.add(MinetestRelease("5.2", 39))
|
||||
session.add(MinetestRelease("5.3", 39))
|
||||
session.add(LuantiRelease("None", 0))
|
||||
session.add(LuantiRelease("0.4.16/17", 32))
|
||||
session.add(LuantiRelease("5.0", 37))
|
||||
session.add(LuantiRelease("5.1", 38))
|
||||
session.add(LuantiRelease("5.2", 39))
|
||||
session.add(LuantiRelease("5.3", 39))
|
||||
|
||||
tags = {}
|
||||
for tag in ["Inventory", "Mapgen", "Building",
|
||||
@@ -69,8 +69,8 @@ def populate_test_data(session):
|
||||
licenses = { x.name : x for x in License.query.all() }
|
||||
tags = { x.name : x for x in Tag.query.all() }
|
||||
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
|
||||
v4 = MinetestRelease.query.filter_by(protocol=32).first()
|
||||
v51 = MinetestRelease.query.filter_by(protocol=38).first()
|
||||
v4 = LuantiRelease.query.filter_by(protocol=32).first()
|
||||
v51 = LuantiRelease.query.filter_by(protocol=38).first()
|
||||
|
||||
ez = User("Shara")
|
||||
ez.github_username = "Ezhh"
|
||||
|
||||
@@ -10,8 +10,8 @@ as it was submitted as university coursework. To learn about the history and dev
|
||||
|
||||
ContentDB is open source software, licensed under AGPLv3.0.
|
||||
|
||||
<a href="https://github.com/minetest/contentdb/" class="btn btn-primary me-1">Source code</a>
|
||||
<a href="https://github.com/minetest/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
|
||||
<a href="https://github.com/luanti-org/contentdb/" class="btn btn-primary me-1">Source code</a>
|
||||
<a href="https://github.com/luanti-org/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
|
||||
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
|
||||
{% if monitoring_url -%}
|
||||
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a>
|
||||
@@ -25,13 +25,13 @@ poor user experience, especially for first-time users.
|
||||
ContentDB isn't just about supporting the in-game content downloader; it's common for technical users to find
|
||||
and review packages using the ContentDB website, but install using Git rather than the in-game installer.
|
||||
**ContentDB's purpose is to be a well-formatted source of information about mods, games,
|
||||
and texture packs for Minetest**.
|
||||
and texture packs for Luanti**.
|
||||
|
||||
## How do I learn how to make mods and games for Minetest?
|
||||
## How do I learn how to make mods and games for Luanti?
|
||||
|
||||
You should read
|
||||
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
|
||||
for a guide to making mods and games using Minetest.
|
||||
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
|
||||
for a guide to making mods and games using Luanti.
|
||||
|
||||
|
||||
<h2 id="donate">How can I support / donate to ContentDB?</h2>
|
||||
@@ -45,5 +45,5 @@ For more information about the cost of ContentDB and what rubenwardy does, see h
|
||||
|
||||
## Sponsorships
|
||||
|
||||
Minetest and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
|
||||
Luanti and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
|
||||
This provides us with improved error logging and performance insights.
|
||||
|
||||
@@ -4,7 +4,7 @@ toc: False
|
||||
|
||||
## Rules
|
||||
|
||||
* [Rules](/rules/)
|
||||
* [Terms of Service](/terms/)
|
||||
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
|
||||
|
||||
## General Help
|
||||
|
||||
@@ -3,7 +3,7 @@ title: API
|
||||
|
||||
## Resources
|
||||
|
||||
* [How the Minetest client uses the API](https://github.com/minetest/contentdb/blob/master/docs/minetest_client.md)
|
||||
* [How the Luanti client uses the API](https://github.com/luanti-org/contentdb/blob/master/docs/luanti_client.md)
|
||||
|
||||
|
||||
## Responses and Error Handling
|
||||
@@ -54,7 +54,7 @@ The response will be a dictionary with the following keys:
|
||||
Not all endpoints require authentication, but it is done using Bearer tokens:
|
||||
|
||||
```bash
|
||||
curl https://content.minetest.net/api/whoami/ \
|
||||
curl https://content.luanti.org/api/whoami/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
@@ -67,8 +67,8 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
||||
* DELETE `/api/delete-token/`: Deletes the currently used token.
|
||||
|
||||
```bash
|
||||
# Logout
|
||||
curl -X DELETE https://content.minetest.net/api/delete-token/ \
|
||||
# Logout
|
||||
curl -X DELETE https://content.luanti.org/api/delete-token/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
@@ -78,9 +78,12 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
|
||||
* GET `/api/packages/` (List)
|
||||
* See [Package Queries](#package-queries)
|
||||
* GET `/api/packages/<username>/<name>/` (Read)
|
||||
* Redirects a JSON object with the keys documented by the PUT endpoint, below.
|
||||
* Plus:
|
||||
* `forum_url`: String or null.
|
||||
* PUT `/api/packages/<author>/<name>/` (Update)
|
||||
* Requires authentication.
|
||||
* JSON dictionary with any of these keys (all are optional, null to delete Nullables):
|
||||
* JSON object with any of these keys (all are optional, null to delete Nullables):
|
||||
* `type`: One of `GAME`, `MOD`, `TXP`.
|
||||
* `title`: Human-readable title.
|
||||
* `name`: Technical name (needs permission if already approved).
|
||||
@@ -99,9 +102,13 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
|
||||
* `video_url`: URL to a video.
|
||||
* `donate_url`: URL to a donation page.
|
||||
* `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,
|
||||
* Returns a JSON object with:
|
||||
* `success`
|
||||
* `package`: updated package
|
||||
* `was_modified`: bool, whether anything changed
|
||||
* GET `/api/packages/<username>/<name>/for-client/`
|
||||
* Similar to the read endpoint, but optimised for the Minetest client
|
||||
* Similar to the read endpoint, but optimised for the Luanti client
|
||||
* `long_description` is given as a hypertext object, see `/hypertext/` below.
|
||||
* `info_hypertext` is the info sidebar as a hypertext object.
|
||||
* Query arguments
|
||||
@@ -109,17 +116,31 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
|
||||
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
|
||||
* `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/<username>/<name>/for-client/reviews/`
|
||||
* Returns hypertext representing the package's reviews
|
||||
* Query arguments
|
||||
* `formspec_version`: Required. See /hypertext/ below.
|
||||
* Returns JSON dictionary with following keys:
|
||||
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
|
||||
* `body`: markup for long description.
|
||||
* `links`: dictionary of anchor name to link URL.
|
||||
* `images`: dictionary of img name to image URL.
|
||||
* `image_tooltips`: dictionary of img name to tooltip text.
|
||||
* The hypertext body contains some placeholders that should be replaced client-side:
|
||||
* `<thumbsup>` with a thumbs up icon.
|
||||
* `<neutral>` with a thumbs up icon.
|
||||
* `<thumbsdown>` with a thumbs up icon.
|
||||
* 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 [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
|
||||
to be used in a `hypertext` formspec element.
|
||||
* Query arguments:
|
||||
* `formspec_version`: Required, maximum supported formspec version.
|
||||
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
|
||||
* Returns JSON dictionary with following key:
|
||||
* Returns JSON dictionary with following keys:
|
||||
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
|
||||
* `body`: markup for long description.
|
||||
* `links`: dictionary of anchor name to link URL.
|
||||
* `images`: dictionary of img name to image URL
|
||||
* `images`: dictionary of img name to image URL.
|
||||
* `image_tooltips`: dictionary of img name to tooltip text.
|
||||
* GET `/api/packages/<username>/<name>/dependencies/`
|
||||
* Returns dependencies, with suggested candidates
|
||||
@@ -163,20 +184,20 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
|
||||
You can download a package by building one of the two URLs:
|
||||
|
||||
```
|
||||
https://content.minetest.net/packages/${author}/${name}/download/`
|
||||
https://content.minetest.net/packages/${author}/${name}/releases/${release}/download/`
|
||||
https://content.luanti.org/packages/${author}/${name}/download/`
|
||||
https://content.luanti.org/packages/${author}/${name}/releases/${release}/download/`
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Edit package
|
||||
curl -X PUT https://content.minetest.net/api/packages/username/name/ \
|
||||
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }'
|
||||
|
||||
# Remove website URL
|
||||
curl -X PUT https://content.minetest.net/api/packages/username/name/ \
|
||||
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{ "website": null }'
|
||||
```
|
||||
@@ -198,8 +219,8 @@ Filter query parameters:
|
||||
* `license`: Filter by [license name](#licenses). Multiple licenses are OR-ed together, ie: `&license=MIT&license=LGPL-2.1-only`
|
||||
* `game`: Filter by [Game Support](/help/game_support/), ex: `Warr1024/nodecore`. (experimental, doesn't show items that support every game currently).
|
||||
* `lang`: Filter by translation support, eg: `en`/`de`/`ja`/`zh_TW`.
|
||||
* `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`.
|
||||
* `protocol_version`: Only show packages supported by this Luanti protocol version.
|
||||
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
|
||||
|
||||
Sorting query parameters:
|
||||
|
||||
@@ -212,7 +233,7 @@ Format query parameters:
|
||||
* `limit`: Return at most `limit` packages.
|
||||
* `fmt`: How the response is formatted.
|
||||
* `keys`: author/name only.
|
||||
* `short`: stuff needed for the Minetest client.
|
||||
* `short`: stuff needed for the Luanti client.
|
||||
* `vcs`: `short` but with `repo`.
|
||||
|
||||
|
||||
@@ -232,8 +253,8 @@ Format query parameters:
|
||||
* `url`: download URL
|
||||
* `commit`: commit hash or null
|
||||
* `downloads`: number of downloads
|
||||
* `min_minetest_version`: dict or null, minimum supported minetest version (inclusive).
|
||||
* `max_minetest_version`: dict or null, minimum supported minetest version (inclusive).
|
||||
* `min_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
|
||||
* `max_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
|
||||
* `size`: size of zip file, in bytes.
|
||||
* `package`
|
||||
* `author`: author username
|
||||
@@ -242,8 +263,8 @@ Format query parameters:
|
||||
* GET `/api/updates/` (Look-up table)
|
||||
* Returns a look-up table from package key (`author/name`) to latest release id
|
||||
* Query arguments
|
||||
* `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`.
|
||||
* `protocol_version`: Only show packages supported by this Luanti protocol version.
|
||||
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
|
||||
* GET `/api/packages/<username>/<name>/releases/` (List)
|
||||
* Returns array of release dictionaries, see above, but without package info.
|
||||
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
|
||||
@@ -258,7 +279,7 @@ Format query parameters:
|
||||
* For zip upload release creation:
|
||||
* `file`: multipart file to upload, like `<input type="file" name="file">`.
|
||||
* `commit`: (Optional) Source Git commit hash, for informational purposes.
|
||||
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
|
||||
* You can set min and max Luanti Versions [using the content's .conf file](/help/package_config/).
|
||||
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
|
||||
* Requires authentication.
|
||||
* Deletes release.
|
||||
@@ -267,7 +288,7 @@ Examples:
|
||||
|
||||
```bash
|
||||
# Create release from Git
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"method": "git",
|
||||
@@ -278,17 +299,17 @@ curl -X POST https://content.minetest.net/api/packages/username/name/releases/ne
|
||||
}'
|
||||
|
||||
# Create release from zip upload
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/file.zip
|
||||
|
||||
# Create release from zip upload with commit hash
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F commit="8ef74deec170a8ce789f6055a59d43876d16a7ea" -F file=@path/to/file.zip
|
||||
|
||||
# Delete release
|
||||
curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/3/ \
|
||||
curl -X DELETE https://content.luanti.org/api/packages/username/name/releases/3/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
@@ -329,26 +350,26 @@ Examples:
|
||||
|
||||
```bash
|
||||
# Create screenshot
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/screnshot.png
|
||||
|
||||
# Create screenshot and set it as the cover image
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
|
||||
|
||||
# Delete screenshot
|
||||
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
|
||||
curl -X DELETE https://content.luanti.org/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/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/order/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d "[13, 2, 5, 7]"
|
||||
|
||||
# Set cover image
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/cover-image/ \
|
||||
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/cover-image/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d "{ 'cover_image': 123 }"
|
||||
```
|
||||
@@ -458,7 +479,7 @@ Supported query parameters:
|
||||
## Collections
|
||||
|
||||
* GET `/api/collections/`
|
||||
* Query args:
|
||||
* Query args:
|
||||
* `author`: collection author username.
|
||||
* `package`: collections that contain the package.
|
||||
* Returns JSON array of collection entries:
|
||||
@@ -468,7 +489,7 @@ Supported query parameters:
|
||||
* `short_description`
|
||||
* `created_at`: creation time in iso format.
|
||||
* `private`: whether collection is private, boolean.
|
||||
* `package_count`: number of packages, integer.
|
||||
* `package_count`: number of packages, integer.
|
||||
* GET `/api/collections/<username>/<name>/`
|
||||
* Returns JSON object for collection:
|
||||
* `author`: author username.
|
||||
@@ -498,7 +519,7 @@ Supported query parameters:
|
||||
### Content Warnings
|
||||
|
||||
* GET `/api/content_warnings/` ([View](/api/content_warnings/))
|
||||
* List of objects with
|
||||
* List of objects with
|
||||
* `name`: technical name
|
||||
* `title`: human-readable title
|
||||
* `description`: tag description or null
|
||||
@@ -506,14 +527,14 @@ Supported query parameters:
|
||||
### Licenses
|
||||
|
||||
* GET `/api/licenses/` ([View](/api/licenses/))
|
||||
* List of objects with:
|
||||
* List of objects with:
|
||||
* `name`
|
||||
* `is_foss`: whether the license is foss
|
||||
|
||||
### Minetest Versions
|
||||
### Luanti Versions
|
||||
|
||||
* GET `/api/minetest_versions/` ([View](/api/minetest_versions/))
|
||||
* List of objects with:
|
||||
* List of objects with:
|
||||
* `name`: Version name.
|
||||
* `is_dev`: boolean, is dev version.
|
||||
* `protocol_version`: protocol version number.
|
||||
@@ -521,7 +542,7 @@ Supported query parameters:
|
||||
### Languages
|
||||
|
||||
* GET `/api/languages/` ([View](/api/languages/))
|
||||
* List of objects with:
|
||||
* List of objects with:
|
||||
* `id`: language code.
|
||||
* `title`: native language name.
|
||||
* `has_contentdb_translation`: whether ContentDB has been translated into this language.
|
||||
@@ -552,13 +573,11 @@ Supported query parameters:
|
||||
* `pop_txp`: popular textures
|
||||
* `pop_game`: popular games
|
||||
* `high_reviewed`: highest reviewed
|
||||
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
|
||||
* `featured`: featured games
|
||||
* GET `/api/cdb_schema/` ([View](/api/cdb_schema/))
|
||||
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
|
||||
* See [JSON Schema Reference](https://json-schema.org/).
|
||||
* POST `/api/hypertext/`
|
||||
* Converts HTML or Markdown to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
|
||||
* Converts HTML or Markdown to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
|
||||
to be used in a `hypertext` formspec element.
|
||||
* Post data: HTML or Markdown as plain text.
|
||||
* Content-Type: `text/html` or `text/markdown`.
|
||||
|
||||
@@ -8,8 +8,8 @@ Expand on the title with the short description. You have a limited number
|
||||
of characters, use them wisely!
|
||||
|
||||
```ini
|
||||
# Bad, we know this is a mod for Minetest. Doesn't give much information other than "food"
|
||||
description = The food mod for Minetest
|
||||
# Bad, we know this is a mod for Luanti. Doesn't give much information other than "food"
|
||||
description = The food mod for Luanti
|
||||
# Much better, says what is actually in this mod!
|
||||
description = Adds soup, cakes, bakes and juices
|
||||
```
|
||||
@@ -20,7 +20,7 @@ A good thumbnail goes a long way to making a package more appealing. It's one of
|
||||
a user sees before clicking on your package. Make sure it's possible to tell what a
|
||||
thumbnail is when it's small.
|
||||
|
||||
For a preview of what your package will look like inside Minetest, see
|
||||
For a preview of what your package will look like inside Luanti, see
|
||||
Edit Package > Screenshots.
|
||||
|
||||
## Screenshots
|
||||
@@ -36,7 +36,7 @@ The target audience of your package page is end users.
|
||||
The long description should explain what your package is about,
|
||||
why the user should choose it, and how to use it if they download it.
|
||||
|
||||
[NodeCore](https://content.minetest.net/packages/Warr1024/nodecore/) is a good
|
||||
[NodeCore](https://content.luanti.org/packages/Warr1024/nodecore/) is a good
|
||||
example of what to do. For inspiration, you might want to look at how games on
|
||||
Steam write their descriptions.
|
||||
|
||||
@@ -55,20 +55,20 @@ The following are redundant and should probably not be included:
|
||||
* API reference (unless your mod is a library only)
|
||||
* Development instructions for your package (this should be in the repo's README)
|
||||
* Screenshots that are already uploaded (unless you want to embed a recipe image in a specific place)
|
||||
* Note: you should avoid images in the long description as they won't be visible inside Minetest,
|
||||
* Note: you should avoid images in the long description as they won't be visible inside Luanti,
|
||||
when support for showing the long description is added.
|
||||
|
||||
## Localize / Translate your package
|
||||
|
||||
According to Google Play, 64% of Minetest Android users don't have English as their main language.
|
||||
According to Google Play, 64% of Luanti Android users don't have English as their main language.
|
||||
Adding translation support to your package increases accessibility. Using content translation, you
|
||||
can also translate your ContentDB page. See Edit Package > Translation for more information.
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
|
||||
{{ _("Translation - Minetest Modding Book") }}
|
||||
{{ _("Translation - Luanti Modding Book") }}
|
||||
</a>
|
||||
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
|
||||
<a class="btn btn-primary" href="https://api.luanti.org/translations/#translating-content-meta">
|
||||
{{ _("Translating content meta - lua_api.md") }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ your client to use new flags.
|
||||
|
||||
## Flags
|
||||
|
||||
Minetest allows you to specify a comma-separated list of flags to hide in the
|
||||
Luanti allows you to specify a comma-separated list of flags to hide in the
|
||||
client:
|
||||
|
||||
```
|
||||
@@ -17,7 +17,7 @@ A flag can be:
|
||||
|
||||
* `nonfree`: can be used to hide packages which do not qualify as
|
||||
'free software', as defined by the Free Software Foundation.
|
||||
* `wip`: packages marked as Work in Progress
|
||||
* `wip`: packages marked as Work in Progress
|
||||
* `deprecated`: packages marked as Deprecated
|
||||
* A content warning, given below.
|
||||
* `*`: hides all content warnings.
|
||||
@@ -33,8 +33,8 @@ without making a release.
|
||||
Packages with mature content will be tagged with a content warning based
|
||||
on the content type.
|
||||
|
||||
* `alcohol_tobacco`: alcohol or tobacco.
|
||||
* `bad_language`: swearing.
|
||||
* `drugs`: drugs or alcohol.
|
||||
* `gambling`
|
||||
* `gore`: blood, etc.
|
||||
* `horror`: shocking and scary content.
|
||||
|
||||
@@ -48,7 +48,7 @@ It's common to do this in README.md or LICENSE.md like so:
|
||||
* conquer_arrow_*.png from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC0 1.0.
|
||||
* conquer_arrow.b3d from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC-BY-SA 3.0.
|
||||
* conquer_arrow_head.png from MTG, CC-BY-SA 3.0.
|
||||
* health_*.png from [Gauges](https://content.minetest.net/packages/Calinou/gauges/) by Calinou, CC0.
|
||||
* health_*.png from [Gauges](https://content.luanti.org/packages/Calinou/gauges/) by Calinou, CC0.
|
||||
```
|
||||
|
||||
if you have a lot of media, then you can split it up by author like so:
|
||||
@@ -75,7 +75,7 @@ Your Name, CC BY-SA 4.0:
|
||||
* [Kenney game assets](https://www.kenney.nl/assets) - everything
|
||||
* [Free Sound](https://freesound.org/) - sounds
|
||||
* [PolyHaven](https://polyhaven.com/) - 3d models and textures.
|
||||
* Other Minetest mods/games
|
||||
* Other Luanti mods/games
|
||||
|
||||
Don't assume the author has correctly licensed their work.
|
||||
Make sure they have clearly indicated the source in a list [like above](#list-the-sources-of-your-media).
|
||||
@@ -141,7 +141,7 @@ permanent bans.
|
||||
|
||||
## Where can I get help?
|
||||
|
||||
[Join](https://www.minetest.net/get-involved/) IRC, Matrix, or Discord to ask for help.
|
||||
[Join](https://www.luanti.org/get-involved/) IRC, Matrix, or Discord to ask for help.
|
||||
In Discord, there are the #assets or #contentdb channels. In IRC or Matrix, you can just ask in the main channels.
|
||||
|
||||
If your package is already on ContentDB, you can open a thread.
|
||||
|
||||
@@ -48,11 +48,11 @@ There are a number of methods:
|
||||
* [Webhooks](/help/release_webhooks/): you can configure your Git host to send a webhook to ContentDB, and create an update immediately.
|
||||
* the [API](/help/api/): This is especially powerful when combined with CI/CD and other API endpoints.
|
||||
|
||||
### How do I learn how to make mods and games for Minetest?
|
||||
### How do I learn how to make mods and games for Luanti?
|
||||
|
||||
You should read
|
||||
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
|
||||
for a guide to making mods and games using Minetest.
|
||||
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
|
||||
for a guide to making mods and games using Luanti.
|
||||
|
||||
### How do I install something from here?
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ title: Featured Packages
|
||||
## What are Featured Packages?
|
||||
|
||||
Featured Packages are shown at the top of the ContentDB homepage. In the future,
|
||||
featured packages may be shown inside the Minetest client.
|
||||
featured packages may be shown inside the Luanti client.
|
||||
|
||||
The purpose is to promote content that demonstrates a high quality of what is
|
||||
possible in Minetest. The selection should be varied, and should vary over time.
|
||||
possible in Luanti. The selection should be varied, and should vary over time.
|
||||
The featured content should be content that we are comfortable recommending to
|
||||
a first time player.
|
||||
|
||||
@@ -47,7 +47,7 @@ other packages to be featured, or for another reason.
|
||||
|
||||
* MUST: Be 100% free and open source (as marked as Free on ContentDB).
|
||||
* MUST: Work out-of-the-box (no weird setup or settings required).
|
||||
* MUST: Be compatible with the latest stable Minetest release.
|
||||
* MUST: Be compatible with the latest stable Luanti release.
|
||||
* SHOULD: Use public source control (such as Git).
|
||||
* SHOULD: Have at least 3 reviews, and be largely positive.
|
||||
|
||||
@@ -94,7 +94,7 @@ is available.
|
||||
### Usability
|
||||
|
||||
* MUST: Unsupported mapgens are disabled in game.conf.
|
||||
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Minetest) wouldn't get completely
|
||||
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Luanti) wouldn't get completely
|
||||
stuck within the first 5 minutes of playing.
|
||||
* SHOULD: Have good documentation. This may include one or more of:
|
||||
* A craftguide, or other in-game learning system
|
||||
|
||||
@@ -11,6 +11,6 @@ You can follow updates from ContentDB in your RSS feed reader. If in doubt, copy
|
||||
Follow new releases for a package:
|
||||
|
||||
```
|
||||
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.atom
|
||||
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.json
|
||||
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.atom
|
||||
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.json
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ You can use `unsupported_games` to specify games that your package doesn't work
|
||||
with, which is useful for overriding ContentDB's automatic detection.
|
||||
|
||||
Both of these are comma-separated lists of game technical ids. Any `_game`
|
||||
suffixes are ignored, just like in Minetest.
|
||||
suffixes are ignored, just like in Luanti.
|
||||
|
||||
supported_games = minetest_game, repixture
|
||||
unsupported_games = lordofthetest, nodecore, whynot
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
title: How to install mods, games, and texture packs
|
||||
description: A guide to installing mods, games, and texture packs in Minetest.
|
||||
description: A guide to installing mods, games, and texture packs in Luanti.
|
||||
|
||||
## Installing from the main menu (recommended)
|
||||
|
||||
@@ -7,8 +7,8 @@ description: A guide to installing mods, games, and texture packs in Minetest.
|
||||
|
||||
1. Open the mainmenu
|
||||
2. Go to the Content tab and click "Browse online content".
|
||||
If you don't see this, then you need to update Minetest to v5.
|
||||
3. Search for the package you want to install, and click "Install".
|
||||
If you don't see this, then you need to update Luanti to v5.
|
||||
3. Search for the package you want to install, and click "Install".
|
||||
4. When installing a mod, you may be shown a dialog about dependencies here.
|
||||
Make sure the base game dropdown box is correct, and then click "Install".
|
||||
|
||||
@@ -16,7 +16,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
|
||||
<div class="col-md-6">
|
||||
<figure>
|
||||
<a href="/static/installing_content_tab.png">
|
||||
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in minetest">
|
||||
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in Luanti">
|
||||
</a>
|
||||
<figcaption class="text-muted ps-1">
|
||||
1. Click Browser Online Content in the content tab.
|
||||
@@ -26,7 +26,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
|
||||
<div class="col-md-6">
|
||||
<figure>
|
||||
<a href="/static/installing_cdb_dialog.png">
|
||||
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in minetest">
|
||||
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in Luanti">
|
||||
</a>
|
||||
<figcaption class="text-muted ps-1">
|
||||
2. Search for the package and click "Install".
|
||||
@@ -38,7 +38,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
|
||||
Troubleshooting:
|
||||
|
||||
* I can't find it in the ContentDB dialog (Browse online content)
|
||||
* Make sure that you're on the latest version of Minetest.
|
||||
* Make sure that you're on the latest version of Luanti.
|
||||
* Are you using Android? Packages with content warnings are hidden by default on android,
|
||||
you can show them by removing `android_default` from the `contentdb_flag_blacklist` setting.
|
||||
* Does the webpage show "Non-free" warnings? Non-free content is hidden by default from all clients,
|
||||
@@ -51,14 +51,14 @@ Troubleshooting:
|
||||
|
||||
1. Mods: Enable the content using "Select Mods" when selecting a world.
|
||||
2. Games: choose a game when making a world.
|
||||
3. Texture packs: Content > Select pack > Click enable.
|
||||
3. Texture packs: Content > Select pack > Click enable.
|
||||
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6">
|
||||
<figure>
|
||||
<a href="/static/installing_select_mods.png">
|
||||
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Minetest">
|
||||
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Luanti">
|
||||
</a>
|
||||
<figcaption class="text-muted ps-1">
|
||||
Enable mods using the Select Mods dialog.
|
||||
@@ -76,7 +76,7 @@ Troubleshooting:
|
||||
3. Find the user data directory.
|
||||
In 5.4.0 and above, you can click "Open user data directory" in the Credits tab.
|
||||
Otherwise:
|
||||
* Windows: whereever you extracted or installed Minetest to.
|
||||
* Windows: wherever you extracted or installed Luanti to.
|
||||
* Linux: usually `~/.minetest/`
|
||||
4. Open or create the folder for the type of content (`mods`, `games`, or `textures`)
|
||||
5. Git clone there
|
||||
|
||||
@@ -13,7 +13,7 @@ and they will be subject to limited promotion.
|
||||
**ContentDB does not allow certain non-free licenses, and will limit the promotion
|
||||
of packages with non-free licenses.**
|
||||
|
||||
Minetest is free and open source software, and is only as big as it is now
|
||||
Luanti is free and open source software, and is only as big as it is now
|
||||
because of this. It's pretty amazing you can take nearly any published mod and modify it
|
||||
to how you like - add some features, maybe fix some bugs - and then share those
|
||||
modifications without the worry of legal issues. The project, itself, relies on open
|
||||
@@ -24,9 +24,9 @@ If you have played nearly any game with a large modding scene, you will find
|
||||
that most mods are legally ambiguous. A lot of them don't even provide the
|
||||
source code to allow you to bug fix or extend as you need.
|
||||
|
||||
Limiting the promotion of problematic licenses helps Minetest avoid ending up in
|
||||
Limiting the promotion of problematic licenses helps Luanti avoid ending up in
|
||||
such a state. Licenses that prohibit redistribution or modification are
|
||||
completely banned from ContentDB and the Minetest forums. Other non-free licenses
|
||||
completely banned from ContentDB and the Luanti forums. Other non-free licenses
|
||||
will be subject to limited promotion - they won't be shown by default in
|
||||
the client.
|
||||
|
||||
@@ -37,7 +37,7 @@ you spread it.
|
||||
## What's so bad about licenses that forbid commercial use?
|
||||
|
||||
Please read [reasons not to use a Creative Commons -NC license](https://freedomdefined.org/Licenses/NC).
|
||||
Here's a quick summary related to Minetest content:
|
||||
Here's a quick summary related to Luanti content:
|
||||
|
||||
1. They make your work incompatible with a growing body of free content, even if
|
||||
you do want to allow derivative works or combinations.
|
||||
@@ -68,7 +68,7 @@ Users can opt in to showing non-free software, if they wish:
|
||||
|
||||
The [`platform_default` flag](/help/content_flags/) is used to control what content
|
||||
each platforms shows. It doesn't hide anything on Desktop, but hides all mature
|
||||
content on Android. You may wish to remove all text from that setting completely,
|
||||
content on Android. You may wish to remove all text from that setting completely,
|
||||
leaving it blank. See [Content Warnings](/help/content_flags/#content-warnings)
|
||||
for information on mature content.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ ContentDB supports the Authorization Code OAuth2 method.
|
||||
Get the user to open the following URL in a web browser:
|
||||
|
||||
```
|
||||
https://content.minetest.net/oauth/authorize/
|
||||
https://content.luanti.org/oauth/authorize/
|
||||
?response_type=code
|
||||
&client_id={CLIENT_ID}
|
||||
&redirect_uri={REDIRECT_URL}
|
||||
@@ -52,7 +52,7 @@ Next, you'll need to exchange the auth for an access token.
|
||||
Do this by making a POST request to the `/oauth/token/` API:
|
||||
|
||||
```bash
|
||||
curl -X POST https://content.minetest.net/oauth/token/ \
|
||||
curl -X POST https://content.luanti.org/oauth/token/ \
|
||||
-F grant_type=authorization_code \
|
||||
-F client_id="CLIENT_ID" \
|
||||
-F client_secret="CLIENT_SECRET" \
|
||||
@@ -98,6 +98,6 @@ Possible errors:
|
||||
Next, you should check the access token works by getting the user information:
|
||||
|
||||
```bash
|
||||
curl https://content.minetest.net/api/whoami/ \
|
||||
curl https://content.luanti.org/api/whoami/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
@@ -42,8 +42,8 @@ ContentDB understands the following information:
|
||||
* `description` - A short description to show in the client.
|
||||
* `depends` - Comma-separated hard dependencies.
|
||||
* `optional_depends` - Comma-separated soft dependencies.
|
||||
* `min_minetest_version` - The minimum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
|
||||
* `max_minetest_version` - The maximum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
|
||||
* `min_minetest_version` - The minimum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
|
||||
* `max_minetest_version` - The maximum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
|
||||
|
||||
and for mods only:
|
||||
|
||||
@@ -68,7 +68,7 @@ It should be a JSON dictionary with one or more of the following optional keys:
|
||||
* `tags`: List of tag names, see [/api/tags/](/api/tags/).
|
||||
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
|
||||
* `license`: A license name, see [/api/licenses/](/api/licenses/).
|
||||
* `media_license`: A license name.
|
||||
* `media_license`: A license name.
|
||||
* `long_description`: Long markdown description.
|
||||
* `repo`: Source repository (eg: Git).
|
||||
* `website`: Website URL.
|
||||
@@ -106,11 +106,11 @@ See [Git Update Detection](/help/update_config/).
|
||||
You can also use [GitLab/GitHub webhooks](/help/release_webhooks/) or the [API](/help/api/)
|
||||
to create releases.
|
||||
|
||||
### Min and Max Minetest Versions
|
||||
### Min and Max Luanti Versions
|
||||
|
||||
<a name="min_max_versions" />
|
||||
|
||||
When creating a release, the `.conf` file will be read to determine what Minetest
|
||||
When creating a release, the `.conf` file will be read to determine what Luanti
|
||||
versions the release supports. If the `.conf` doesn't specify, then it is assumed
|
||||
that it supports all versions.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ The process is as follows:
|
||||
3. The git host posts a webhook notification to ContentDB, using the API token assigned to it.
|
||||
4. ContentDB checks the API token and issues a new release.
|
||||
* If multiple packages match, then only the first will have a release created.
|
||||
|
||||
|
||||
### Branch filtering
|
||||
|
||||
By default, "New commit" or "push" based webhooks will only work on "master"/"main" branches.
|
||||
@@ -39,7 +39,7 @@ Tag-based webhooks are accepted on any branch.
|
||||
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
|
||||
2. Copy the access token that was generated.
|
||||
3. Go to the GitLab repository's settings > Webhooks > Add Webhook.
|
||||
4. Set the payload URL to `https://content.minetest.net/github/webhook/`
|
||||
4. Set the payload URL to `https://content.luanti.org/github/webhook/`
|
||||
5. Set the content type to JSON.
|
||||
6. Set the secret to the access token that you copied.
|
||||
7. Set the events
|
||||
@@ -53,7 +53,7 @@ Tag-based webhooks are accepted on any branch.
|
||||
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
|
||||
2. Copy the access token that was generated.
|
||||
3. Go to the GitLab repository's settings > Webhooks.
|
||||
4. Set the URL to `https://content.minetest.net/gitlab/webhook/`
|
||||
4. Set the URL to `https://content.luanti.org/gitlab/webhook/`
|
||||
6. Set the secret token to the ContentDB access token that you copied.
|
||||
7. Set the events
|
||||
* If you want a rolling release, choose "Push events".
|
||||
@@ -67,5 +67,5 @@ Tag-based webhooks are accepted on any branch.
|
||||
See the [Package Configuration and Releases Guide](/help/package_config/) for
|
||||
documentation on configuring the release creation.
|
||||
|
||||
From the Git repository, you can set the min/max Minetest versions, which files are included,
|
||||
From the Git repository, you can set the min/max Luanti versions, which files are included,
|
||||
and update the package meta.
|
||||
|
||||
@@ -33,4 +33,4 @@ downloaded from that IP.
|
||||
You can see all scores using the [scores REST API](/api/scores/), or by
|
||||
using the [Prometheus metrics](/help/metrics/) endpoint.
|
||||
|
||||
Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
|
||||
Consider [suggesting improvements](https://github.com/luanti-org/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
|
||||
|
||||
@@ -39,5 +39,5 @@ Clicking "Save" on "Update Settings" will mark a package as up-to-date.
|
||||
See the [Package Configuration and Releases Guide](/help/package_config/) for
|
||||
documentation on configuring the release creation.
|
||||
|
||||
From the Git repository, you can set the min/max Minetest versions, which files are included,
|
||||
From the Git repository, you can set the min/max Luanti versions, which files are included,
|
||||
and update the package meta.
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
title: WTFPL is a terrible license
|
||||
toc: False
|
||||
|
||||
<div id="warning" class="alert alert-warning">
|
||||
<span class="icon_message"></span>
|
||||
|
||||
Please reconsider the choice of WTFPL as a license.
|
||||
|
||||
<script>
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
var params = new URLSearchParams(location.search);
|
||||
var r = params.get("r");
|
||||
if (r) {
|
||||
document.write("<a class='alert_right button' href='" + r + "'>Okay</a>");
|
||||
} else {
|
||||
document.getElementById("warning").style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
The use of WTFPL as a license is discouraged for multiple reasons.
|
||||
|
||||
* **No Warranty disclaimer:** This could open you up to being sued.<sup>[1]</sup>
|
||||
@@ -37,4 +18,4 @@ license, saying:<sup>[3]</sup>
|
||||
|
||||
1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html)
|
||||
2. [FSF](https://www.gnu.org/licenses/license-list.en.html)
|
||||
3. [OSI](https://opensource.org/minutes20090304)
|
||||
3. [OSI](https://opensource.org/meeting-minutes/minutes20090304)
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
title: Package Inclusion Policy and Guidance
|
||||
|
||||
## 0. Overview
|
||||
|
||||
ContentDB is for the community, and as such listings should be useful to the
|
||||
community. To help with this, there are a few rules to improve the quality of
|
||||
the listings and to combat abuse.
|
||||
|
||||
* **No inappropriate content.** <sup>2.1</sup>
|
||||
* **Content must be playable/useful, but not necessarily finished.** <sup>2.2</sup>
|
||||
* **Don't use the name of another mod unless your mod is a fork or reimplementation.** <sup>3</sup>
|
||||
* **Licenses must allow derivatives, redistribution, and must not discriminate.** <sup>4</sup>
|
||||
* **Don't put promotions or advertisements in any package metadata.** <sup>5</sup>
|
||||
* **Don't manipulate package placement using reviews or downloads.** <sup>6</sup>
|
||||
* **Screenshots must not be misleading.** <sup>7</sup>
|
||||
* **The ContentDB admin reserves the right to remove packages for any reason**,
|
||||
including ones not covered by this document, and to ban users who abuse
|
||||
this service. <sup>1</sup>
|
||||
|
||||
|
||||
## 1. General
|
||||
|
||||
@@ -26,33 +9,53 @@ including ones not covered by this document, and to ban users who abuse this ser
|
||||
|
||||
## 2. Accepted Content
|
||||
|
||||
### 2.1. Acceptable Content
|
||||
### 2.1. Mature Content
|
||||
|
||||
Sexually-orientated content is not permitted.
|
||||
If in doubt at what this means, [contact us by raising a report](/report/).
|
||||
See the [Terms of Service](/terms/) for a full list of prohibited content.
|
||||
|
||||
Mature content is permitted providing that it is labelled correctly.
|
||||
See [Content Flags](/help/content_flags/).
|
||||
Other mature content is permitted providing that it is labelled with the applicable
|
||||
[content warning](/help/content_flags/).
|
||||
|
||||
### 2.2. State of Completion
|
||||
### 2.2. Useful Content / State of Completion
|
||||
|
||||
ContentDB should only currently contain playable content - content which is
|
||||
sufficiently complete to be useful to end-users. It's fine to add stuff which is
|
||||
still a Work in Progress (WIP) as long as it adds sufficient value; Note that
|
||||
this doesn't mean that you should add a thing you started working on yesterday,
|
||||
it's worth adding all the basic stuff to make your package useful.
|
||||
ContentDB is for playable and useful content - content which is sufficiently
|
||||
complete to be useful to end-users.
|
||||
|
||||
You should make sure to mark Work in Progress stuff as such in the "maintenance
|
||||
status" column, as this will help advise players.
|
||||
It's fine to add stuff which is still a Work in Progress (WIP) as long as it
|
||||
adds sufficient value. You must make sure to mark Work in Progress stuff as
|
||||
such in the "maintenance status" dropdown, as this will help advise players.
|
||||
|
||||
Adding non-player facing mods, such as libraries and server tools, is perfectly
|
||||
fine and encouraged. ContentDB isn't just for player-facing things, and adding
|
||||
libraries allows them to be installed when a mod depends on it.
|
||||
fine and encouraged. ContentDB isn't just for player-facing things and adding
|
||||
libraries allows Luanti to automatically install dependencies.
|
||||
|
||||
### 2.3. Language
|
||||
|
||||
We require packages to be in English with (optional) client-side translations for
|
||||
other languages. This is because Luanti currently requires English as the base language
|
||||
([Issue to change this](https://github.com/luanti-org/luanti/issues/6503)).
|
||||
|
||||
Your package's title and short description must be in English. You can use client-side
|
||||
translations to [translate content meta](https://api.luanti.org/translations/#translating-content-meta).
|
||||
|
||||
### 2.4. Attempt to contribute before forking
|
||||
|
||||
You should attempt to contribute upstream before forking a package. If you choose
|
||||
to fork, you should have a justification (different objectives, maintainer is unavailable, etc).
|
||||
You should use a different title and make it clear in the long description what the
|
||||
benefit of your fork is over the original package.
|
||||
|
||||
### 2.5. Copyright and trademarks
|
||||
|
||||
Your package must not violate copyright or trademarks. You should avoid the use of
|
||||
trademarks in the package's title or short description. If you do use a trademark,
|
||||
ensure that you phrase it in a way that does not imply official association or
|
||||
endorsement.
|
||||
|
||||
|
||||
## 3. Technical Names
|
||||
|
||||
### 3.1 Right to a name
|
||||
### 3.1. Right to a Name
|
||||
|
||||
A package uses a name when it has that name or contains a mod that uses that name.
|
||||
|
||||
@@ -70,23 +73,46 @@ to change the name of the package, or your package won't be accepted.
|
||||
|
||||
We reserve the right to issue exceptions for this where we feel necessary.
|
||||
|
||||
### 3.2. Mod Forks and Reimplementations
|
||||
### 3.2. Forks and Reimplementations
|
||||
|
||||
An exception to the above is that mods are allowed to have the same name as a
|
||||
mod if it's a fork of that mod (or a close reimplementation). In real terms, it
|
||||
should be possible to use the new mod as a drop-in replacement.
|
||||
must be possible to use the new mod as a drop-in replacement.
|
||||
|
||||
We reserve the right to decide whether a mod counts as a fork or
|
||||
reimplementation of the mod that owns the name.
|
||||
|
||||
### 3.3. Game Mod Namespacing
|
||||
|
||||
New mods introduced by a game must have a unique common prefix to avoid conflicts with
|
||||
other games and standalone mods. For example, the NodeCore game's first-party mods all
|
||||
start with `nc_`: `nc_api`, `nc_doors`.
|
||||
|
||||
You may include existing or standard mods in your game without renaming them to use the
|
||||
namespace. For example, NodeCore could include the `awards` mod without needing to rename it.
|
||||
|
||||
Standalone mods may not use a game's namespace unless they have been given permission by
|
||||
the game's author.
|
||||
|
||||
The exception given by 3.2 also applies to game namespaces - you may use another game's
|
||||
prefix if your game is a fork.
|
||||
|
||||
|
||||
## 4. Licenses
|
||||
|
||||
### 4.1. Allowed Licenses
|
||||
### 4.1. License file
|
||||
|
||||
You must have a LICENSE, LICENSE.txt, or LICENSE.md file describing the licensing of your package.
|
||||
Please ensure that you correctly credit any resources (code, assets, or otherwise)
|
||||
that you have used in your package. For help on doing copyright correctly, see
|
||||
the [Copyright help page](/help/copyright/).
|
||||
that you have used in your package.
|
||||
|
||||
You may use lowercase or include a suffix in the filename (ie: `license-code.txt`). If
|
||||
you are making a game or modpack, your top level license file may just be a summary or
|
||||
refer to the license files of individual components.
|
||||
|
||||
For help on doing copyright correctly, see the [Copyright help page](/help/copyright/).
|
||||
|
||||
### 4.2. Allowed Licenses
|
||||
|
||||
**The use of licenses that do not allow derivatives or redistribution is not
|
||||
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
|
||||
@@ -96,18 +122,18 @@ of the content on servers or singleplayer is also not permitted.**
|
||||
However, closed sourced licenses are allowed if they allow the above.
|
||||
|
||||
If the license you use is not on the list then please select "Other", and we'll
|
||||
get around to adding it. We tend to reject custom/untested licenses, and
|
||||
reserve the right to decide whether a license should be included.
|
||||
get around to adding it. We reject custom/untested licenses and reserve the right
|
||||
to decide whether a license should be included.
|
||||
|
||||
Please note that the definitions of "free" and "non-free" is the same as that
|
||||
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
|
||||
|
||||
### 4.2. Recommended Licenses
|
||||
### 4.3. Recommended Licenses
|
||||
|
||||
It is highly recommended that you use a Free and Open Source software (FOSS)
|
||||
license. FOSS licenses result in a sharing community and will increase the
|
||||
number of potential users your package has. Using a closed source license will
|
||||
result in your package not being shown in Minetest by default. See the help page
|
||||
result in your package not being shown in Luanti by default. See the help page
|
||||
on [non-free licenses](/help/non_free/) for more information.
|
||||
|
||||
It is recommended that you use a proper license for code with a warranty
|
||||
@@ -150,10 +176,14 @@ Doing so may result in temporary or permanent suspension from ContentDB.
|
||||
|
||||
## 7. Screenshots
|
||||
|
||||
1. **Screenshots must not violate copyright.** You should have the rights to the
|
||||
screenshot.
|
||||
1. We require all packages to have at least one screenshot. For packages without visual
|
||||
content, we recommend making a symbolic image with icons, graphics, or text to depict
|
||||
the package.
|
||||
|
||||
2. **Screenshots must depict the actual content of the package in some way, and
|
||||
2. **Screenshots must not violate copyright.** This means don't just copy images
|
||||
from Google search, see [the copyright guide](/help/copyright/).
|
||||
|
||||
3. **Screenshots must depict the actual content of the package in some way, and
|
||||
not be misleading.**
|
||||
|
||||
Do not use idealized mockups or blender concept renders if they do not
|
||||
@@ -169,20 +199,9 @@ Doing so may result in temporary or permanent suspension from ContentDB.
|
||||
will look like in a typical/realistic game scene, but should be "in the
|
||||
background" only as far as possible.
|
||||
|
||||
3. **Screenshots must only contain content appropriate for the Content Warnings of
|
||||
4. **Screenshots must only contain content appropriate for the Content Warnings of
|
||||
the package.**
|
||||
|
||||
4. **Screenshots should be MOSTLY in-game screenshots, if applicable.** Some
|
||||
alterations on in-game screenshots are okay, such as collages, added text,
|
||||
some reasonable compositing.
|
||||
|
||||
Don't just use one of the textures from the package; show it in-situ as it
|
||||
actually looks in the game.
|
||||
|
||||
5. **Packages should have a screenshot when reasonably applicable.**
|
||||
|
||||
6. **Screenshots should be of reasonable dimensions.** We recommend using 1920x1080.
|
||||
|
||||
|
||||
## 8. Security
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ title: Privacy Policy
|
||||
---
|
||||
|
||||
Last Updated: 2024-04-30
|
||||
([View updates](https://github.com/minetest/contentdb/commits/master/app/flatpages/privacy_policy.md))
|
||||
([View updates](https://github.com/luanti-org/contentdb/commits/master/app/flatpages/privacy_policy.md))
|
||||
|
||||
## What Information is Collected
|
||||
|
||||
@@ -56,7 +56,7 @@ Please avoid giving other personal information as we do not want it.
|
||||
|
||||
* Only the admin has access to the HTTP requests.
|
||||
The logs may be shared with others to aid in debugging, but care will be taken to remove any personal information.
|
||||
* Encrypted backups may be shared with selected Minetest staff members (moderators + core devs).
|
||||
* Encrypted backups may be shared with selected Luanti staff members (moderators + core devs).
|
||||
The keys and the backups themselves are given to different people,
|
||||
requiring at least two staff members to read a backup.
|
||||
* Email addresses are visible to moderators and the admin.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
title: Rules
|
||||
|
||||
The following are the rules for user behaviour on ContentDB, including reviews,
|
||||
threads, comments, and profiles. For packages, see the
|
||||
[Package Inclusion Policy](/policy_and_guidance/).
|
||||
|
||||
1. **Be respectful:** attacks towards any person or group, slurs,
|
||||
trolling/baiting, and other toxic behavior are not welcome.
|
||||
2. **Assume good faith:** communication over the Internet is hard, try to assume
|
||||
good faith when eg: responding to reviews.
|
||||
3. **No sexual content** and ensure you keep discussion appropriate given the
|
||||
package's [content warnings](/help/content_flags/).
|
||||
|
||||
You can report things by clicking [report](/report/) in the footer of pages you
|
||||
want to report.
|
||||
133
app/flatpages/terms.md
Normal file
133
app/flatpages/terms.md
Normal file
@@ -0,0 +1,133 @@
|
||||
title: Terms of Service
|
||||
|
||||
Also see the [Package Inclusion Policy](/policy_and_guidance/).
|
||||
|
||||
## Content
|
||||
|
||||
### Prohibited content
|
||||
|
||||
You must not post/transmit anything which is illegal under the laws in any part of the United Kingdom.
|
||||
|
||||
You must not (or use the service to) facilitate or commit any offence under the laws in any part of the United Kingdom.
|
||||
|
||||
This includes, in particular, terrorism content (as set out in Schedule 5, Online Safety Act 2023),
|
||||
child sexual exploitation and abuse content (as set out in Schedule 6, Online Safety Act 2023), and
|
||||
content that amounts to an offence specified in Schedule 7, Online Safety Act 2023.
|
||||
|
||||
Prohibited content includes:
|
||||
|
||||
* Pornographic content. This includes content of such a nature that it is reasonable to assume that it was produced
|
||||
solely or principally for the purpose of sexual arousal.
|
||||
* Content which encourages, promotes or provides instructions for suicide
|
||||
* Content which encourages, promotes or provides instructions for an act of deliberate self-injury
|
||||
* Content which encourages, promotes or provides instructions for an eating disorder or behaviours associated with an eating disorder
|
||||
* Content which is abusive and which targets any of the following characteristics: race, religion, sex,
|
||||
sexual orientation, disability, gender reassignment.
|
||||
* Content which incites hatred against people:
|
||||
* of a particular race, religion, sex or sexual orientation
|
||||
* who have a disability
|
||||
* who have the characteristic of gender reassignment
|
||||
* Content which encourages, promotes or provides instructions for an act of serious violence against a person
|
||||
* Bullying content
|
||||
* Content which:
|
||||
* depicts real or realistic serious violence against a person
|
||||
* depicts the real or realistic serious injury of a person in graphic detail
|
||||
* Content which:
|
||||
* depicts real or realistic serious violence against an animal
|
||||
* depicts the real or realistic serious injury of an animal in graphic detail
|
||||
* realistically depicts serious violence against a fictional creature or the serious injury of a fictional
|
||||
creature in graphic detail
|
||||
* Content which encourages, promotes or provides instructions for a challenge or stunt highly likely to result in
|
||||
serious injury to the person who does it or to someone else
|
||||
* Content which encourages a person to ingest, inject, inhale or in any other way self-administer:
|
||||
* a physically harmful substance
|
||||
* a substance in such a quantity as to be physically harmful
|
||||
|
||||
### Protecting users from illegal content
|
||||
|
||||
We provide this service free of charge, and on the basis that we may:
|
||||
|
||||
* take down, or restrict access to, anything that you generate, upload or share; and
|
||||
* suspend or ban you from using all or part of the service
|
||||
|
||||
if we think that this is reasonable to protect you, other users, the service, or us. This applies, in particular,
|
||||
to prohibited content.
|
||||
|
||||
If we are alerted by a person to the presence of any illegal content, or we become aware of it in any other way,
|
||||
we will swiftly take down that content.
|
||||
|
||||
To minimise the length of time for which any illegal content within the scope of the Online Safety Act 2023 is present:
|
||||
|
||||
* in respect of terrorism content, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
|
||||
* in respect of child sexual exploitation or abuse content, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
|
||||
* in respect of other content that amounts to an offence specified in Schedule 7, Online Safety Act 2023, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
|
||||
|
||||
### Protecting children
|
||||
|
||||
We protect all children from the kinds of content listed in "Prohibited Content" by:
|
||||
|
||||
* prohibiting that type of content from our service; and
|
||||
* swiftly taking down that content, if we are alerted by a person to its presence, or we become aware of it in any other way.
|
||||
|
||||
### Proactive technology
|
||||
|
||||
We do not use proactive technology to detect illegal content.
|
||||
|
||||
|
||||
## Limitation of Liability
|
||||
|
||||
THE SERVICE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
||||
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL WE BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SERVICE OR THE USE OR OTHER DEALINGS IN THE SERVICE.
|
||||
|
||||
We reserve the right to ban or suspend your account, or take down your content, for any reason.
|
||||
|
||||
|
||||
## Jurisdiction and Governing Law
|
||||
|
||||
This service is subject to the jurisdiction of the United Kingdom.
|
||||
|
||||
|
||||
## Complaints
|
||||
|
||||
### Reporting content
|
||||
|
||||
You may report content by clicking the report flag next to a comment or "Report" on the page containing the content.
|
||||
|
||||
You can also make reports by [contacting the admin]({{ admin_contact_url }}).
|
||||
|
||||
### Complaints and Appeals
|
||||
|
||||
You may send a complaint / request an appeal by [contacting the admin]({{ admin_contact_url }}).
|
||||
|
||||
### Your right to bring a claim
|
||||
|
||||
This clause applies only to users within the United Kingdom.
|
||||
|
||||
The Online Safety Act 2023 says that you have a right to bring a claim for breach of contract if:
|
||||
|
||||
* anything that you generate, upload or share is taken down, or access to it is restricted, in breach of the terms of service, or
|
||||
* you are suspended or banned from using the service in breach of the terms of service.
|
||||
|
||||
This does not apply to emails, SMS messages, MMS messages, one-to-one live aural communications,
|
||||
comments and reviews (together with any further comments on such comments or reviews), or content which identifies
|
||||
you as a user (e.g. a user name or profile picture).
|
||||
|
||||
Whether or not a contract exists between you and us is a question of fact. If we do not have a contractual
|
||||
relationship with you in respect of the service, there can be no breach of contract and, as such, this cannot apply.
|
||||
|
||||
It is for a court to determine:
|
||||
|
||||
- if there is a contract between you and us and, if so, its terms
|
||||
- if there has been a breach by us of that contract
|
||||
- if that breach has caused you any recoverable loss
|
||||
- the size (e.g. value) of your loss
|
||||
|
||||
This clause is subject to "Limitation of liability" and "Jurisdiction".
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This terms of service was written based on [a template](https://onlinesafetyact.co.uk/online_safety_act_terms/)
|
||||
created by Neil Brown, CC BY-SA 4.0.
|
||||
@@ -96,7 +96,7 @@ def _get_approval_statistics(entries: list[AuditLogEntry], start_date: Optional[
|
||||
(end_date is None or entry.created_at <= end_date)))
|
||||
info.is_in_range = info.is_in_range or is_in_range
|
||||
|
||||
new_state = get_state(entry.title)
|
||||
new_state = get_state(entry.title.replace("…", "") + (entry.description or ""))
|
||||
if new_state == info.state:
|
||||
continue
|
||||
|
||||
|
||||
@@ -174,12 +174,6 @@ class GameSupport:
|
||||
|
||||
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
|
||||
if package.id_ in visited:
|
||||
first_idx = visited.index(package.id_)
|
||||
visited = visited[first_idx:]
|
||||
err = f"Dependency cycle detected: {' -> '.join(visited)} -> {package.id_}"
|
||||
for id_ in visited:
|
||||
package2 = self.get(id_)
|
||||
package2.add_error(err)
|
||||
return None
|
||||
|
||||
if package.type == PackageType.GAME:
|
||||
|
||||
@@ -46,6 +46,9 @@ class PackageValidationNote:
|
||||
self.buttons.append((url, label))
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.message)
|
||||
|
||||
|
||||
def is_package_name_taken(normalised_name: str) -> bool:
|
||||
return Package.query.filter(
|
||||
@@ -107,8 +110,7 @@ def validate_package_for_approval(package: Package) -> List[PackageValidationNot
|
||||
# Don't bother validating any more until we have a release
|
||||
return retval
|
||||
|
||||
if (package.type == PackageType.GAME or package.type == PackageType.TXP) and \
|
||||
package.screenshots.count() == 0:
|
||||
if package.screenshots.count() == 0:
|
||||
danger(lazy_gettext("You need to add at least one screenshot."))
|
||||
|
||||
missing_deps = package.get_missing_hard_dependencies_query().all()
|
||||
|
||||
@@ -69,6 +69,19 @@ ALLOWED_FIELDS = {
|
||||
"translation_url": str,
|
||||
}
|
||||
|
||||
NULLABLE = {
|
||||
"tags",
|
||||
"content_warnings",
|
||||
"repo",
|
||||
"website",
|
||||
"issue_tracker",
|
||||
"issueTracker",
|
||||
"forums",
|
||||
"video_url",
|
||||
"donate_url",
|
||||
"translation_url",
|
||||
}
|
||||
|
||||
ALIASES = {
|
||||
"short_description": "short_desc",
|
||||
"issue_tracker": "issueTracker",
|
||||
@@ -86,11 +99,13 @@ def is_int(val):
|
||||
|
||||
def validate(data: dict):
|
||||
for key, value in data.items():
|
||||
if value is not None:
|
||||
if value is None:
|
||||
check(key in NULLABLE, f"{key} must not be null")
|
||||
else:
|
||||
typ = ALLOWED_FIELDS.get(key)
|
||||
check(typ is not None, key + " is not a known field")
|
||||
check(typ is not None, f"{key} is not a known field")
|
||||
if typ != AnyType:
|
||||
check(isinstance(value, typ), key + " must be a " + typ.__name__)
|
||||
check(isinstance(value, typ), f"{key} must be a " + typ.__name__)
|
||||
|
||||
if "name" in data:
|
||||
name = data["name"]
|
||||
@@ -102,12 +117,12 @@ def validate(data: dict):
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
check(value.startswith("http://") or value.startswith("https://"),
|
||||
key + " must start with http:// or https://")
|
||||
check(validators.url(value), key + " must be a valid URL")
|
||||
f"{key} must start with http:// or https://")
|
||||
check(validators.url(value), f"{key} must be a valid URL")
|
||||
|
||||
|
||||
def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, data: dict,
|
||||
reason: str = None):
|
||||
reason: str = None) -> bool:
|
||||
if not package.check_perm(user, Permission.EDIT_PACKAGE):
|
||||
raise LogicError(403, lazy_gettext("You don't have permission to edit this package"))
|
||||
|
||||
@@ -121,6 +136,9 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
||||
|
||||
for alias, to in ALIASES.items():
|
||||
if alias in data:
|
||||
if to in data and data[to] != data[alias]:
|
||||
raise LogicError(403, f"Aliased field ({alias}) does not match new field ({to})")
|
||||
|
||||
data[to] = data[alias]
|
||||
|
||||
validate(data)
|
||||
@@ -169,7 +187,6 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
||||
package.provides.append(m)
|
||||
|
||||
if "tags" in data:
|
||||
old_tags = list(package.tags)
|
||||
package.tags.clear()
|
||||
for tag_id in (data["tags"] or []):
|
||||
if is_int(tag_id):
|
||||
@@ -192,9 +209,14 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
||||
raise LogicError(400, "Unknown warning: " + warning_id)
|
||||
package.content_warnings.append(warning)
|
||||
|
||||
if not was_new:
|
||||
was_modified = was_new
|
||||
if was_new:
|
||||
msg = f"Created package {package.author.username}/{package.name}"
|
||||
add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
|
||||
else:
|
||||
after_dict = package.as_dict("/")
|
||||
diff = diff_dictionaries(before_dict, after_dict)
|
||||
was_modified = len(diff) > 0
|
||||
|
||||
if reason is None:
|
||||
msg = "Edited {}".format(package.title)
|
||||
@@ -208,6 +230,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
||||
severity = AuditSeverity.NORMAL if user in package.maintainers else AuditSeverity.EDITOR
|
||||
add_audit_log(severity, user, msg, package.get_url("packages.view"), package, json.dumps(diff, indent=4))
|
||||
|
||||
db.session.commit()
|
||||
if was_modified:
|
||||
db.session.commit()
|
||||
|
||||
return package
|
||||
return was_modified
|
||||
|
||||
@@ -23,12 +23,12 @@ from flask_babel import lazy_gettext
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.logic.uploads import upload_file
|
||||
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease
|
||||
from app.models import PackageRelease, db, Permission, User, Package, LuantiRelease
|
||||
from app.tasks.importtasks import make_vcs_release, check_zip_release
|
||||
from app.utils import AuditSeverity, add_audit_log, nonempty_or_none, normalize_line_endings
|
||||
|
||||
|
||||
def check_can_create_release(user: User, package: Package):
|
||||
def check_can_create_release(user: User, package: Package, name: str):
|
||||
if not package.check_perm(user, Permission.MAKE_RELEASE):
|
||||
raise LogicError(403, lazy_gettext("You don't have permission to make releases"))
|
||||
|
||||
@@ -37,10 +37,13 @@ def check_can_create_release(user: User, package: Package):
|
||||
if count >= 5:
|
||||
raise LogicError(429, lazy_gettext("You've created too many releases for this package in the last 5 minutes, please wait before trying again"))
|
||||
|
||||
if PackageRelease.query.filter_by(package_id=package.id, name=name).count() > 0:
|
||||
raise LogicError(403, lazy_gettext("A release with this name already exists"))
|
||||
|
||||
|
||||
def do_create_vcs_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None):
|
||||
check_can_create_release(user, package)
|
||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None):
|
||||
check_can_create_release(user, package, name)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
@@ -67,9 +70,9 @@ def do_create_vcs_release(user: User, package: Package, name: str, title: Option
|
||||
|
||||
|
||||
def do_create_zip_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None,
|
||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None,
|
||||
commit_hash: str = None):
|
||||
check_can_create_release(user, package)
|
||||
check_can_create_release(user, package, name)
|
||||
|
||||
if commit_hash:
|
||||
commit_hash = commit_hash.lower()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import imghdr
|
||||
import os
|
||||
|
||||
from flask_babel import lazy_gettext
|
||||
from flask_babel import lazy_gettext, LazyString
|
||||
|
||||
from app import app
|
||||
from app.logic.LogicError import LogicError
|
||||
@@ -35,7 +35,7 @@ def is_allowed_image(data):
|
||||
return imghdr.what(None, data) in ALLOWED_IMAGES
|
||||
|
||||
|
||||
def upload_file(file, file_type, file_type_desc):
|
||||
def upload_file(file, file_type: str, file_type_desc: LazyString | str, length: int=10):
|
||||
if not file or file is None or file.filename == "":
|
||||
raise LogicError(400, "Expected file")
|
||||
|
||||
@@ -62,7 +62,7 @@ def upload_file(file, file_type, file_type_desc):
|
||||
|
||||
file.stream.seek(0)
|
||||
|
||||
filename = random_string(10) + "." + ext
|
||||
filename = random_string(length) + "." + ext
|
||||
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||
file.save(filepath)
|
||||
|
||||
|
||||
214
app/markdown.py
214
app/markdown.py
@@ -1,214 +0,0 @@
|
||||
# ContentDB
|
||||
# Copyright (C) rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import bleach
|
||||
from bleach import Cleaner
|
||||
from bleach.linkifier import LinkifyFilter
|
||||
from bs4 import BeautifulSoup
|
||||
from markdown import Markdown
|
||||
from flask import url_for
|
||||
from jinja2.utils import markupsafe
|
||||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import SimpleTagInlineProcessor
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.extensions.codehilite import CodeHiliteExtension
|
||||
from xml.etree import ElementTree
|
||||
|
||||
# Based on
|
||||
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
|
||||
#
|
||||
# License: MIT
|
||||
|
||||
ALLOWED_TAGS = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "hr",
|
||||
"ul", "ol", "li",
|
||||
"p",
|
||||
"br",
|
||||
"pre",
|
||||
"code",
|
||||
"blockquote",
|
||||
"strong",
|
||||
"em",
|
||||
"a",
|
||||
"img",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"div", "span", "del", "s",
|
||||
"details",
|
||||
"summary",
|
||||
}
|
||||
|
||||
ALLOWED_CSS = [
|
||||
"highlight", "codehilite",
|
||||
"hll", "c", "err", "g", "k", "l", "n", "o", "x", "p", "ch", "cm", "cp", "cpf", "c1", "cs",
|
||||
"gd", "ge", "gr", "gh", "gi", "go", "gp", "gs", "gu", "gt", "kc", "kd", "kn", "kp", "kr",
|
||||
"kt", "ld", "m", "s", "na", "nb", "nc", "no", "nd", "ni", "ne", "nf", "nl", "nn", "nx",
|
||||
"py", "nt", "nv", "ow", "w", "mb", "mf", "mh", "mi", "mo", "sa", "sb", "sc", "dl", "sd",
|
||||
"s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il",
|
||||
]
|
||||
|
||||
|
||||
def allow_class(_tag, name, value):
|
||||
return name == "class" and value in ALLOWED_CSS
|
||||
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
"h1": ["id"],
|
||||
"h2": ["id"],
|
||||
"h3": ["id"],
|
||||
"h4": ["id"],
|
||||
"a": ["href", "title", "data-username"],
|
||||
"img": ["src", "title", "alt"],
|
||||
"code": allow_class,
|
||||
"div": allow_class,
|
||||
"span": allow_class,
|
||||
"table": ["id"],
|
||||
}
|
||||
|
||||
ALLOWED_PROTOCOLS = {"http", "https", "mailto"}
|
||||
|
||||
md = None
|
||||
|
||||
|
||||
def linker_callback(attrs, new=False):
|
||||
if new:
|
||||
text = attrs.get("_text")
|
||||
if not (text.startswith("http://") or text.startswith("https://")):
|
||||
return None
|
||||
return attrs
|
||||
|
||||
|
||||
def render_markdown(source):
|
||||
html = md.convert(source)
|
||||
|
||||
cleaner = Cleaner(
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
filters=[partial(LinkifyFilter,
|
||||
callbacks=[linker_callback] + bleach.linkifier.DEFAULT_CALLBACKS,
|
||||
skip_tags={"pre", "code"})])
|
||||
return cleaner.clean(html)
|
||||
|
||||
|
||||
class DelInsExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
del_proc = SimpleTagInlineProcessor(r"(\~\~)(.+?)(\~\~)", "del")
|
||||
md.inlinePatterns.register(del_proc, "del", 200)
|
||||
|
||||
ins_proc = SimpleTagInlineProcessor(r"(\+\+)(.+?)(\+\+)", "ins")
|
||||
md.inlinePatterns.register(ins_proc, "ins", 200)
|
||||
|
||||
|
||||
RE_PARTS = dict(
|
||||
USER=r"[A-Za-z0-9._-]*\b",
|
||||
REPO=r"[A-Za-z0-9_]+\b"
|
||||
)
|
||||
|
||||
|
||||
class MentionPattern(Pattern):
|
||||
ANCESTOR_EXCLUDES = ("a",)
|
||||
|
||||
def __init__(self, config, md):
|
||||
MENTION_RE = r"(@({USER})(?:\/({REPO}))?)".format(**RE_PARTS)
|
||||
super(MentionPattern, self).__init__(MENTION_RE, md)
|
||||
self.config = config
|
||||
|
||||
def handleMatch(self, m):
|
||||
from app.models import User
|
||||
|
||||
label = m.group(2)
|
||||
user = m.group(3)
|
||||
package_name = m.group(4)
|
||||
if package_name:
|
||||
el = ElementTree.Element("a")
|
||||
el.text = label
|
||||
el.set("href", url_for("packages.view", author=user, name=package_name))
|
||||
return el
|
||||
else:
|
||||
if User.query.filter_by(username=user).count() == 0:
|
||||
return None
|
||||
|
||||
el = ElementTree.Element("a")
|
||||
el.text = label
|
||||
el.set("href", url_for("users.profile", username=user))
|
||||
el.set("data-username", user)
|
||||
return el
|
||||
|
||||
|
||||
class MentionExtension(Extension):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MentionExtension, self).__init__(*args, **kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
md.ESCAPED_CHARS.append("@")
|
||||
md.inlinePatterns.register(MentionPattern(self.getConfigs(), md), "mention", 20)
|
||||
|
||||
|
||||
MARKDOWN_EXTENSIONS = ["fenced_code", "tables", CodeHiliteExtension(guess_lang=False), "toc", DelInsExtension(), MentionExtension()]
|
||||
MARKDOWN_EXTENSION_CONFIG = {
|
||||
"fenced_code": {},
|
||||
"tables": {}
|
||||
}
|
||||
|
||||
|
||||
def init_markdown(app):
|
||||
global md
|
||||
|
||||
md = Markdown(extensions=MARKDOWN_EXTENSIONS,
|
||||
extension_configs=MARKDOWN_EXTENSION_CONFIG,
|
||||
output_format="html")
|
||||
|
||||
@app.template_filter()
|
||||
def markdown(source):
|
||||
return markupsafe.Markup(render_markdown(source))
|
||||
|
||||
|
||||
def get_headings(html: str):
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
headings = soup.find_all(["h1", "h2", "h3"])
|
||||
|
||||
root = []
|
||||
stack = []
|
||||
for heading in headings:
|
||||
this = {"link": heading.get("id") or "", "text": heading.text, "children": []}
|
||||
this_level = int(heading.name[1:]) - 1
|
||||
|
||||
while this_level <= len(stack):
|
||||
stack.pop()
|
||||
|
||||
if len(stack) > 0:
|
||||
stack[-1]["children"].append(this)
|
||||
else:
|
||||
root.append(this)
|
||||
|
||||
stack.append(this)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def get_user_mentions(html: str) -> set:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
links = soup.select("a[data-username]")
|
||||
return set([x.get("data-username") for x in links])
|
||||
|
||||
|
||||
def get_links(html: str, url: str) -> set:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
links = soup.select("a[href]")
|
||||
return set([urljoin(url, x.get("href")) for x in links])
|
||||
112
app/markdown/__init__.py
Normal file
112
app/markdown/__init__.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# ContentDB
|
||||
# Copyright (C) rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Sequence
|
||||
from bs4 import BeautifulSoup
|
||||
from jinja2.utils import markupsafe
|
||||
from markdown_it import MarkdownIt
|
||||
from markdown_it.common.utils import unescapeAll, escapeHtml
|
||||
from markdown_it.token import Token
|
||||
from markdown_it.presets import gfm_like
|
||||
from mdit_py_plugins.anchors import anchors_plugin
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
from pygments.util import ClassNotFound
|
||||
from pygments.formatters.html import HtmlFormatter
|
||||
|
||||
from .cleaner import clean_html
|
||||
from .mention import init_mention
|
||||
|
||||
|
||||
def highlight_code(code, name, attrs):
|
||||
try:
|
||||
lexer = get_lexer_by_name(name)
|
||||
except ClassNotFound:
|
||||
return None
|
||||
|
||||
formatter = HtmlFormatter()
|
||||
|
||||
return highlight(code, lexer, formatter)
|
||||
|
||||
|
||||
def render_code(self, tokens: Sequence[Token], idx, options, env):
|
||||
token = tokens[idx]
|
||||
info = unescapeAll(token.info).strip() if token.info else ""
|
||||
langName = info.split(maxsplit=1)[0] if info else ""
|
||||
|
||||
if options.highlight:
|
||||
return options.highlight(
|
||||
token.content, langName, ""
|
||||
) or f"<pre><code>{escapeHtml(token.content)}</code></pre>"
|
||||
|
||||
return f"<pre><code>{escapeHtml(token.content)}</code></pre>"
|
||||
|
||||
|
||||
gfm_like.make()
|
||||
md = MarkdownIt("gfm-like", {"highlight": highlight_code})
|
||||
md.use(anchors_plugin, permalink=True, permalinkSymbol="🔗", max_level=6)
|
||||
md.add_render_rule("fence", render_code)
|
||||
init_mention(md)
|
||||
|
||||
|
||||
def render_markdown(source, clean=True):
|
||||
html = md.render(source)
|
||||
if clean:
|
||||
return clean_html(html)
|
||||
else:
|
||||
return html
|
||||
|
||||
|
||||
def init_markdown(app):
|
||||
@app.template_filter()
|
||||
def markdown(source):
|
||||
return markupsafe.Markup(render_markdown(source))
|
||||
|
||||
|
||||
def get_headings(html: str):
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
headings = soup.find_all(["h1", "h2", "h3"])
|
||||
|
||||
root = []
|
||||
stack = []
|
||||
for heading in headings:
|
||||
text = heading.find(text=True, recursive=False)
|
||||
this = {"link": heading.get("id") or "", "text": text, "children": []}
|
||||
this_level = int(heading.name[1:]) - 1
|
||||
|
||||
while this_level <= len(stack):
|
||||
stack.pop()
|
||||
|
||||
if len(stack) > 0:
|
||||
stack[-1]["children"].append(this)
|
||||
else:
|
||||
root.append(this)
|
||||
|
||||
stack.append(this)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def get_user_mentions(html: str) -> set:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
links = soup.select("a[data-username]")
|
||||
return set([x.get("data-username") for x in links])
|
||||
|
||||
|
||||
def get_links(html: str) -> set:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
links = soup.select("a[href]")
|
||||
return set([x.get("href") for x in links])
|
||||
97
app/markdown/cleaner.py
Normal file
97
app/markdown/cleaner.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# ContentDB
|
||||
# Copyright (C) rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
from bleach import Cleaner
|
||||
from bleach.linkifier import LinkifyFilter, DEFAULT_CALLBACKS
|
||||
|
||||
|
||||
# Based on
|
||||
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
|
||||
#
|
||||
# License: MIT
|
||||
|
||||
ALLOWED_TAGS = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "hr",
|
||||
"ul", "ol", "li",
|
||||
"p",
|
||||
"br",
|
||||
"pre",
|
||||
"code",
|
||||
"blockquote",
|
||||
"strong",
|
||||
"em",
|
||||
"a",
|
||||
"img",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"div", "span", "del", "s",
|
||||
"details",
|
||||
"summary",
|
||||
"sup",
|
||||
}
|
||||
|
||||
ALLOWED_CSS = [
|
||||
"highlight", "codehilite",
|
||||
"hll", "c", "err", "g", "k", "l", "n", "o", "x", "p", "ch", "cm", "cp", "cpf", "c1", "cs",
|
||||
"gd", "ge", "gr", "gh", "gi", "go", "gp", "gs", "gu", "gt", "kc", "kd", "kn", "kp", "kr",
|
||||
"kt", "ld", "m", "s", "na", "nb", "nc", "no", "nd", "ni", "ne", "nf", "nl", "nn", "nx",
|
||||
"py", "nt", "nv", "ow", "w", "mb", "mf", "mh", "mi", "mo", "sa", "sb", "sc", "dl", "sd",
|
||||
"s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il",
|
||||
]
|
||||
|
||||
|
||||
def allow_class(_tag, name, value):
|
||||
return name == "class" and value in ALLOWED_CSS
|
||||
|
||||
|
||||
def allow_a(_tag, name, value):
|
||||
return name in ["href", "title", "data-username"] or (name == "class" and value == "header-anchor")
|
||||
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
"h1": ["id"],
|
||||
"h2": ["id"],
|
||||
"h3": ["id"],
|
||||
"h4": ["id"],
|
||||
"a": allow_a,
|
||||
"img": ["src", "title", "alt"],
|
||||
"code": allow_class,
|
||||
"div": allow_class,
|
||||
"span": allow_class,
|
||||
"table": ["id"],
|
||||
}
|
||||
|
||||
ALLOWED_PROTOCOLS = {"http", "https", "mailto"}
|
||||
|
||||
|
||||
def linker_callback(attrs, new=False):
|
||||
if new:
|
||||
text = attrs.get("_text")
|
||||
if not (text.startswith("http://") or text.startswith("https://")):
|
||||
return None
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
def clean_html(html: str):
|
||||
cleaner = Cleaner(
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
filters=[partial(LinkifyFilter,
|
||||
callbacks=[linker_callback] + DEFAULT_CALLBACKS,
|
||||
skip_tags={"pre", "code"})])
|
||||
return cleaner.clean(html)
|
||||
109
app/markdown/mention.py
Normal file
109
app/markdown/mention.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# ContentDB
|
||||
# Copyright (C) rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from flask import url_for
|
||||
from markdown_it import MarkdownIt
|
||||
from markdown_it.token import Token
|
||||
from markdown_it.rules_core.state_core import StateCore
|
||||
from typing import Sequence, List
|
||||
|
||||
|
||||
def render_user_mention(self, tokens: Sequence[Token], idx, options, env):
|
||||
token = tokens[idx]
|
||||
username = token.content
|
||||
url = url_for("users.profile", username=username)
|
||||
return f"<a href=\"{url}\" data-username=\"{username}\">@{username}</a>"
|
||||
|
||||
|
||||
def render_package_mention(self, tokens: Sequence[Token], idx, options, env):
|
||||
token = tokens[idx]
|
||||
username = token.content
|
||||
name = token.attrs["name"]
|
||||
url = url_for("packages.view", author=username, name=name)
|
||||
return f"<a href=\"{url}\">@{username}/{name}</a>"
|
||||
|
||||
|
||||
def parse_mentions(state: StateCore):
|
||||
for block_token in state.tokens:
|
||||
if block_token.type != "inline" or block_token.children is None:
|
||||
continue
|
||||
|
||||
link_depth = 0
|
||||
html_link_depth = 0
|
||||
|
||||
children = []
|
||||
for token in block_token.children:
|
||||
if token.type == "link_open":
|
||||
link_depth += 1
|
||||
elif token.type == "link_close":
|
||||
link_depth -= 1
|
||||
elif token.type == "html_inline":
|
||||
# is link open / close?
|
||||
pass
|
||||
|
||||
if link_depth > 0 or html_link_depth > 0 or token.type != "text":
|
||||
children.append(token)
|
||||
else:
|
||||
children.extend(split_tokens(token, state))
|
||||
|
||||
block_token.children = children
|
||||
|
||||
|
||||
RE_PARTS = dict(
|
||||
USER=r"[A-Za-z0-9._-]*\b",
|
||||
NAME=r"[A-Za-z0-9_]+\b"
|
||||
)
|
||||
MENTION_RE = r"(@({USER})(?:\/({NAME}))?)".format(**RE_PARTS)
|
||||
|
||||
|
||||
def split_tokens(token: Token, state: StateCore) -> List[Token]:
|
||||
tokens = []
|
||||
content = token.content
|
||||
pos = 0
|
||||
for match in re.finditer(MENTION_RE, content):
|
||||
username = match.group(2)
|
||||
package_name = match.group(3)
|
||||
(start, end) = match.span(0)
|
||||
|
||||
if start > pos:
|
||||
token_text = Token("text", "", 0)
|
||||
token_text.content = content[pos:start]
|
||||
token_text.level = token.level
|
||||
tokens.append(token_text)
|
||||
|
||||
mention = Token("package_mention" if package_name else "user_mention", "", 0)
|
||||
mention.content = username
|
||||
mention.attrSet("name", package_name)
|
||||
mention.level = token.level
|
||||
tokens.append(mention)
|
||||
|
||||
pos = end
|
||||
|
||||
if pos < len(content):
|
||||
token_text = Token("text", "", 0)
|
||||
token_text.content = content[pos:]
|
||||
token_text.level = token.level
|
||||
tokens.append(token_text)
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
def init_mention(md: MarkdownIt):
|
||||
md.add_render_rule("user_mention", render_user_mention, "html")
|
||||
md.add_render_rule("package_mention", render_package_mention, "html")
|
||||
md.core.ruler.after("inline", "mention", parse_mentions)
|
||||
@@ -13,8 +13,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask_babel import LazyString
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy_searchable import make_searchable
|
||||
@@ -125,13 +124,115 @@ class AuditLogEntry(db.Model):
|
||||
raise Exception("Unknown permission given to AuditLogEntry.check_perm()")
|
||||
|
||||
if perm == Permission.VIEW_AUDIT_DESCRIPTION:
|
||||
return user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
|
||||
return (self.package and user in self.package.maintainers) or user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
|
||||
|
||||
|
||||
class ReportCategory(enum.Enum):
|
||||
ACCOUNT_DELETION = "account_deletion"
|
||||
COPYRIGHT = "copyright"
|
||||
USER_CONDUCT = "user_conduct"
|
||||
SPAM = "spam"
|
||||
ILLEGAL_HARMFUL = "illegal_harmful"
|
||||
REVIEW = "review"
|
||||
APPEAL = "appeal"
|
||||
OTHER = "other"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def title(self) -> LazyString:
|
||||
if self == ReportCategory.ACCOUNT_DELETION:
|
||||
return lazy_gettext("Account deletion")
|
||||
elif self == ReportCategory.COPYRIGHT:
|
||||
return lazy_gettext("Copyright infringement / DMCA")
|
||||
elif self == ReportCategory.USER_CONDUCT:
|
||||
return lazy_gettext("User behaviour, bullying, or abuse")
|
||||
elif self == ReportCategory.SPAM:
|
||||
return lazy_gettext("Spam")
|
||||
elif self == ReportCategory.ILLEGAL_HARMFUL:
|
||||
return lazy_gettext("Illegal or harmful content")
|
||||
elif self == ReportCategory.REVIEW:
|
||||
return lazy_gettext("Outdated/invalid review")
|
||||
elif self == ReportCategory.APPEAL:
|
||||
return lazy_gettext("Appeal")
|
||||
elif self == ReportCategory.OTHER:
|
||||
return lazy_gettext("Other")
|
||||
else:
|
||||
raise Exception("Unknown report category")
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
try:
|
||||
return ReportCategory[name.upper()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def choices(cls, with_none):
|
||||
ret = [(choice, choice.title) for choice in cls]
|
||||
|
||||
if with_none:
|
||||
ret.insert(0, (None, ""))
|
||||
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
if item is None or (isinstance(item, str) and item.upper() == "NONE"):
|
||||
return None
|
||||
return item if type(item) == ReportCategory else ReportCategory[item.upper()]
|
||||
|
||||
|
||||
class Report(db.Model):
|
||||
id = db.Column(db.String(24), primary_key=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="reports")
|
||||
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True)
|
||||
thread = db.relationship("Thread", foreign_keys=[thread_id])
|
||||
|
||||
category = db.Column(db.Enum(ReportCategory), nullable=False)
|
||||
url = db.Column(db.String, nullable=True)
|
||||
title = db.Column(db.Unicode(300), nullable=False)
|
||||
message = db.Column(db.UnicodeText, nullable=False)
|
||||
|
||||
is_resolved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
attachments = db.relationship("ReportAttachment", back_populates="report", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
|
||||
def check_perm(self, user, perm):
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Report.check_perm()")
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if perm == Permission.SEE_REPORT:
|
||||
return user.rank.at_least(UserRank.EDITOR)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to reports".format(perm.name))
|
||||
|
||||
|
||||
class ReportAttachment(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
report_id = db.Column(db.String(24), db.ForeignKey("report.id"), nullable=False)
|
||||
report = db.relationship("Report", foreign_keys=[report_id], back_populates="attachments")
|
||||
|
||||
url = db.Column(db.String(100), nullable=False)
|
||||
|
||||
|
||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
|
||||
"minetest.net", "dropboxusercontent.com", "4shared.com",
|
||||
"minetest.net", "luanti.org", "dropboxusercontent.com", "4shared.com",
|
||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
|
||||
"imageshack.com", "imgur.com"]
|
||||
|
||||
@@ -158,7 +259,7 @@ class ForumTopic(db.Model):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.topic_id)
|
||||
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.topic_id)
|
||||
|
||||
def get_repo_url(self):
|
||||
if self.link is None:
|
||||
|
||||
@@ -457,7 +457,7 @@ class Package(db.Model):
|
||||
if self.forums is None:
|
||||
return None
|
||||
|
||||
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.forums)
|
||||
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.forums)
|
||||
|
||||
enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True)
|
||||
|
||||
@@ -679,6 +679,7 @@ class Package(db.Model):
|
||||
"website": self.website,
|
||||
"issue_tracker": self.issueTracker,
|
||||
"forums": self.forums,
|
||||
"forum_url": self.forums_url,
|
||||
"video_url": self.video_url,
|
||||
"video_thumbnail_url": self.get_video_thumbnail_url(True),
|
||||
"donate_url": self.donate_url_actual,
|
||||
@@ -811,7 +812,7 @@ class Package(db.Model):
|
||||
|
||||
elif perm == Permission.APPROVE_SCREENSHOT:
|
||||
return (is_maintainer or is_approver) and \
|
||||
user.rank.at_least(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
|
||||
user.rank.at_least(UserRank.TRUSTED_MEMBER if self.approved else UserRank.NEW_MEMBER)
|
||||
|
||||
elif perm == Permission.EDIT_MAINTAINERS or perm == Permission.DELETE_PACKAGE:
|
||||
return is_owner or user.rank.at_least(UserRank.EDITOR)
|
||||
@@ -1042,7 +1043,7 @@ class Tag(db.Model):
|
||||
}
|
||||
|
||||
|
||||
class MinetestRelease(db.Model):
|
||||
class LuantiRelease(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
protocol = db.Column(db.Integer, nullable=False, default=0)
|
||||
@@ -1066,12 +1067,11 @@ class MinetestRelease(db.Model):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["MinetestRelease"]:
|
||||
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["LuantiRelease"]:
|
||||
if version:
|
||||
parts = version.strip().split(".")
|
||||
if len(parts) >= 2:
|
||||
major_minor = parts[0] + "." + parts[1]
|
||||
query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
|
||||
query = LuantiRelease.query.filter(func.replace(LuantiRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
|
||||
if protocol_num:
|
||||
query = query.filter_by(protocol=protocol_num)
|
||||
|
||||
@@ -1081,9 +1081,9 @@ class MinetestRelease(db.Model):
|
||||
|
||||
if protocol_num:
|
||||
# Find the closest matching release
|
||||
return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol),
|
||||
db.desc(MinetestRelease.id)) \
|
||||
.filter(MinetestRelease.protocol <= protocol_num).first()
|
||||
return LuantiRelease.query.order_by(db.desc(LuantiRelease.protocol),
|
||||
db.desc(LuantiRelease.id)) \
|
||||
.filter(LuantiRelease.protocol <= protocol_num).first()
|
||||
|
||||
return None
|
||||
|
||||
@@ -1103,6 +1103,7 @@ class PackageRelease(db.Model):
|
||||
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||
release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
|
||||
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
@@ -1113,11 +1114,11 @@ class PackageRelease(db.Model):
|
||||
|
||||
return self.release_notes.split("\n")[0]
|
||||
|
||||
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
|
||||
min_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
|
||||
min_rel = db.relationship("LuantiRelease", foreign_keys=[min_rel_id])
|
||||
|
||||
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
|
||||
max_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
|
||||
max_rel = db.relationship("LuantiRelease", foreign_keys=[max_rel_id])
|
||||
|
||||
# If the release is approved, then the task_id must be null and the url must be present
|
||||
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
|
||||
@@ -1126,14 +1127,14 @@ class PackageRelease(db.Model):
|
||||
def file_path(self):
|
||||
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
|
||||
@property
|
||||
def file_size_bytes(self):
|
||||
def calculate_file_size_bytes(self):
|
||||
path = self.file_path
|
||||
if not os.path.isfile(path):
|
||||
return 0
|
||||
self.file_size_bytes = 0
|
||||
return
|
||||
|
||||
file_stats = os.stat(path)
|
||||
return file_stats.st_size
|
||||
self.file_size_bytes = file_stats.st_size
|
||||
|
||||
@property
|
||||
def file_size(self):
|
||||
@@ -1263,6 +1264,8 @@ class PackageScreenshot(db.Model):
|
||||
width = db.Column(db.Integer, nullable=False)
|
||||
height = db.Column(db.Integer, nullable=False)
|
||||
|
||||
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
def is_very_small(self):
|
||||
return self.width < 720 or self.height < 405
|
||||
|
||||
@@ -1276,14 +1279,14 @@ class PackageScreenshot(db.Model):
|
||||
def file_path(self):
|
||||
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
|
||||
@property
|
||||
def file_size_bytes(self):
|
||||
def calculate_file_size_bytes(self):
|
||||
path = self.file_path
|
||||
if not os.path.isfile(path):
|
||||
return 0
|
||||
self.file_size_bytes = 0
|
||||
return
|
||||
|
||||
file_stats = os.stat(path)
|
||||
return file_stats.st_size
|
||||
self.file_size_bytes = file_stats.st_size
|
||||
|
||||
@property
|
||||
def file_size(self):
|
||||
@@ -1368,6 +1371,8 @@ class PackageUpdateConfig(db.Model):
|
||||
# Set to now when an outdated notification is sent. Set to None when a release is created
|
||||
outdated_at = db.Column(db.DateTime, nullable=True, default=None)
|
||||
|
||||
last_checked_at = db.Column(db.DateTime, nullable=True, default=None)
|
||||
|
||||
trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT)
|
||||
ref = db.Column(db.String(41), nullable=True, default=None)
|
||||
|
||||
@@ -1438,7 +1443,7 @@ class PackageDailyStats(db.Model):
|
||||
reason_update = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
@staticmethod
|
||||
def update(package: Package, is_minetest: bool, reason: str):
|
||||
def update(package: Package, is_luanti: bool, reason: str):
|
||||
date = datetime.datetime.utcnow().date()
|
||||
|
||||
to_update = dict()
|
||||
@@ -1446,7 +1451,7 @@ class PackageDailyStats(db.Model):
|
||||
"package_id": package.id, "date": date
|
||||
}
|
||||
|
||||
field_platform = "platform_minetest" if is_minetest else "platform_other"
|
||||
field_platform = "platform_minetest" if is_luanti else "platform_other"
|
||||
to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1
|
||||
kwargs[field_platform] = 1
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ class Thread(db.Model):
|
||||
|
||||
watchers = db.relationship("User", secondary=watchers, backref="watching")
|
||||
|
||||
report = db.relationship("Report", foreign_keys="Report.thread_id", back_populates="thread", lazy="dynamic")
|
||||
|
||||
first_reply = db.relationship("ThreadReply", uselist=False, foreign_keys="ThreadReply.thread_id",
|
||||
lazy=True, order_by=db.asc("id"), viewonly=True,
|
||||
primaryjoin="Thread.id==ThreadReply.thread_id")
|
||||
|
||||
@@ -96,6 +96,7 @@ class Permission(enum.Enum):
|
||||
CHANGE_USERNAMES = "CHANGE_USERNAMES"
|
||||
CHANGE_RANK = "CHANGE_RANK"
|
||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||
LINK_TO_WEBSITE = "LINK_TO_WEBSITE"
|
||||
SEE_THREAD = "SEE_THREAD"
|
||||
CREATE_THREAD = "CREATE_THREAD"
|
||||
COMMENT_THREAD = "COMMENT_THREAD"
|
||||
@@ -114,6 +115,7 @@ class Permission(enum.Enum):
|
||||
EDIT_COLLECTION = "EDIT_COLLECTION"
|
||||
VIEW_COLLECTION = "VIEW_COLLECTION"
|
||||
CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT"
|
||||
SEE_REPORT = "SEE_REPORT"
|
||||
|
||||
# Only return true if the permission is valid for *all* contexts
|
||||
# See Package.check_perm for package-specific contexts
|
||||
@@ -210,6 +212,7 @@ class User(db.Model, UserMixin):
|
||||
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
collections = db.relationship("Collection", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.asc("title"))
|
||||
clients = db.relationship("OAuthClient", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
reports = db.relationship("Report", back_populates="user", lazy="dynamic", cascade="all")
|
||||
|
||||
ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
|
||||
|
||||
@@ -260,7 +263,7 @@ class User(db.Model, UserMixin):
|
||||
return "/static/bot_avatar.png"
|
||||
else:
|
||||
from app.utils.gravatar import get_gravatar
|
||||
return get_gravatar(self.email or f"{self.username}@content.minetest.net")
|
||||
return get_gravatar(self.email or f"{self.username}@content.luanti.org")
|
||||
|
||||
def check_perm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
@@ -287,6 +290,8 @@ class User(db.Model, UserMixin):
|
||||
return user.rank.at_least(UserRank.NEW_MEMBER)
|
||||
else:
|
||||
return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank)
|
||||
elif perm == Permission.LINK_TO_WEBSITE:
|
||||
return user.rank.at_least(UserRank.MEMBER)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||
|
||||
|
||||
119
app/public/funding.json
Normal file
119
app/public/funding.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "v1.0.0",
|
||||
"entity": {
|
||||
"type": "organisation",
|
||||
"role": "maintainer",
|
||||
"name": "Luanti",
|
||||
"email": "rw@rubenwardy.com",
|
||||
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
|
||||
"webpageUrl": {
|
||||
"url": "https://www.luanti.org"
|
||||
}
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"guid": "luanti",
|
||||
"name": "Luanti",
|
||||
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
|
||||
"webpageUrl": {
|
||||
"url": "https://www.luanti.org"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/luanti-org/luanti"
|
||||
},
|
||||
"licenses": [
|
||||
"spdx:LGPL-2.1",
|
||||
"spdx:CC-BY-SA-3.0",
|
||||
"spdx:MIT",
|
||||
"spdx:Apache-2.0"
|
||||
],
|
||||
"tags": [
|
||||
"lua",
|
||||
"voxel",
|
||||
"game"
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "contentdb",
|
||||
"name": "Luanti ContentDB",
|
||||
"description": "A content database for Luanti mods, games, and more.",
|
||||
"webpageUrl": {
|
||||
"url": "https://content.luanti.org/about/"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/luanti-org/contentdb"
|
||||
},
|
||||
"licenses": [
|
||||
"spdx:AGPL-3.0",
|
||||
"spdx:CC-BY-SA-4.0"
|
||||
],
|
||||
"tags": [
|
||||
"python",
|
||||
"flask",
|
||||
"luanti",
|
||||
"minetest"
|
||||
]
|
||||
}
|
||||
],
|
||||
"funding": {
|
||||
"channels": [
|
||||
{
|
||||
"guid": "open-collective",
|
||||
"type": "other",
|
||||
"address": "https://opencollective.com/luanti",
|
||||
"description": "Recurring and one-time donations to Luanti"
|
||||
}
|
||||
],
|
||||
"plans": [
|
||||
{
|
||||
"guid": "oc-eur-backer",
|
||||
"status": "active",
|
||||
"name": "Luanti backer",
|
||||
"description": "Become a backer for €5 per month and help Luanti development",
|
||||
"amount": 5,
|
||||
"currency": "EUR",
|
||||
"frequency": "monthly",
|
||||
"channels": [
|
||||
"open-collective"
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "oc-eur-supporter",
|
||||
"status": "active",
|
||||
"name": "Luanti supporter",
|
||||
"description": "Become a supporter for €5 per month and help Luanti development",
|
||||
"amount": 100,
|
||||
"currency": "EUR",
|
||||
"frequency": "monthly",
|
||||
"channels": [
|
||||
"open-collective"
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "oc-eur-custom",
|
||||
"status": "active",
|
||||
"name": "Luanti custom one-off",
|
||||
"description": "You may donate any amount you're comfortable with",
|
||||
"amount": 0,
|
||||
"currency": "EUR",
|
||||
"frequency": "one-time",
|
||||
"channels": [
|
||||
"open-collective"
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "fosdem",
|
||||
"status": "active",
|
||||
"name": "FOSDEM",
|
||||
"description": "It costs us €3000 to attend FOSDEM",
|
||||
"amount": 3000,
|
||||
"currency": "EUR",
|
||||
"frequency": "one-time",
|
||||
"channels": [
|
||||
"open-collective"
|
||||
]
|
||||
}
|
||||
],
|
||||
"history": []
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ async function load_data() {
|
||||
const data = {
|
||||
datasets: [
|
||||
{ label: "Web / other", data: getData(json.platform_other) },
|
||||
{ label: "Minetest", data: getData(json.platform_minetest) },
|
||||
{ label: "Luanti", data: getData(json.platform_minetest) },
|
||||
],
|
||||
};
|
||||
setup_chart(ctx, data, annotations);
|
||||
|
||||
@@ -26,7 +26,7 @@ window.addEventListener("load", () => {
|
||||
try {
|
||||
const pasteData = e.clipboardData.getData('text');
|
||||
const url = new URL(pasteData);
|
||||
if (url.hostname === "forum.minetest.net") {
|
||||
if (url.hostname === "forum.luanti.org") {
|
||||
forumsField.value = url.searchParams.get("t");
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -37,7 +37,7 @@ window.addEventListener("load", () => {
|
||||
|
||||
const openForums = document.getElementById("forums-button");
|
||||
openForums.addEventListener("click", () => {
|
||||
window.open("https://forum.minetest.net/viewtopic.php?t=" + forumsField.value, "_blank");
|
||||
window.open("https://forum.luanti.org/viewtopic.php?t=" + forumsField.value, "_blank");
|
||||
});
|
||||
|
||||
function setupHints(id, hints) {
|
||||
@@ -68,8 +68,9 @@ window.addEventListener("load", () => {
|
||||
}
|
||||
|
||||
setupHints("short_desc", {
|
||||
"short_desc_mods": (val) => val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0,
|
||||
"short_desc_mods": (val) => val.indexOf("luanti") >= 0 || val.indexOf("minetest") >= 0 ||
|
||||
val.indexOf("mod") >= 0 || val.indexOf("modpack") >= 0 ||
|
||||
val.indexOf("mod pack") >= 0,
|
||||
});
|
||||
|
||||
setupHints("desc", {
|
||||
@@ -85,7 +86,8 @@ window.addEventListener("load", () => {
|
||||
"desc_page_topic": (val) => {
|
||||
const topicId = document.getElementById("forums").value;
|
||||
const r = new RegExp(`forum\\.minetest\\.net\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
|
||||
return topicId && r.test(val);
|
||||
const r2 = new RegExp(`forum\\.luanti\\.org\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
|
||||
return topicId && (r.test(val) || r2.test(val));
|
||||
},
|
||||
"desc_page_repo": (val) => {
|
||||
const repoUrl = document.getElementById("repo").value.replace(".git", "");
|
||||
|
||||
@@ -22,7 +22,7 @@ function sleep(interval) {
|
||||
}
|
||||
|
||||
|
||||
async function pollTask(poll_url, disableTimeout) {
|
||||
async function pollTask(poll_url, disableTimeout, onProgress) {
|
||||
let tries = 0;
|
||||
|
||||
while (true) {
|
||||
@@ -42,6 +42,10 @@ async function pollTask(poll_url, disableTimeout) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (res && res.status) {
|
||||
onProgress?.(res);
|
||||
}
|
||||
|
||||
if (res && res.status === "SUCCESS") {
|
||||
console.log("Got result")
|
||||
return res.result;
|
||||
@@ -62,3 +66,41 @@ async function performTask(url) {
|
||||
throw "Start task didn't return string!";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const taskId = document.querySelector("[data-task-id]")?.getAttribute("data-task-id");
|
||||
if (taskId) {
|
||||
const progress = document.getElementById("progress");
|
||||
|
||||
function onProgress(res) {
|
||||
let status = res.status.toLowerCase();
|
||||
if (status === "progress") {
|
||||
progress.classList.remove("d-none");
|
||||
const bar = progress.children[0];
|
||||
|
||||
const {current, total, running} = res.result;
|
||||
const perc = Math.min(Math.max(100 * current / total, 0), 100);
|
||||
bar.style.width = `${perc}%`;
|
||||
bar.setAttribute("aria-valuenow", current);
|
||||
bar.setAttribute("aria-valuemax", total);
|
||||
|
||||
const packages = (running ?? []).map(x => `${x.author}/${x.name}`).join(", ");
|
||||
document.getElementById("status").textContent = `Status: in progress (${current} / ${total})\n\n${packages}`;
|
||||
} else {
|
||||
progress.classList.add("d-none");
|
||||
|
||||
if (status === "pending") {
|
||||
status = "pending or unknown";
|
||||
}
|
||||
document.getElementById("status").textContent = `Status: ${status}`;
|
||||
}
|
||||
}
|
||||
|
||||
pollTask(`/tasks/${taskId}/`, true, onProgress)
|
||||
.then(function() { location.reload() })
|
||||
.catch(function(e) {
|
||||
console.error(e);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ShortName>ContentDB</ShortName>
|
||||
<LongName>ContentDB</LongName>
|
||||
<InputEncoding>UTF-8</InputEncoding>
|
||||
<Description>Search mods, games, and textures for Minetest.</Description>
|
||||
<Tags>Minetest Mod Game Subgame Search</Tags>
|
||||
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
|
||||
<Description>Search mods, games, and textures for Luanti.</Description>
|
||||
<Tags>Luanti Minetest Mod Game Subgame Search</Tags>
|
||||
<Url type="text/html" method="get" template="https://content.luanti.org/packages?q={searchTerms}"/>
|
||||
</OpenSearchDescription>
|
||||
|
||||
@@ -22,7 +22,7 @@ from sqlalchemy.orm import subqueryload
|
||||
from sqlalchemy.sql.expression import func
|
||||
from sqlalchemy_searchable import search
|
||||
|
||||
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \
|
||||
from .models import db, PackageType, Package, ForumTopic, License, LuantiRelease, PackageRelease, User, Tag, \
|
||||
ContentWarning, PackageState, PackageDevState
|
||||
from .utils import is_yes, get_int_or_abort
|
||||
|
||||
@@ -49,7 +49,7 @@ class QueryBuilder:
|
||||
hide_wip: bool
|
||||
hide_nonfree: bool
|
||||
show_added: bool
|
||||
version: Optional[MinetestRelease]
|
||||
version: Optional[LuantiRelease]
|
||||
has_lang: Optional[str]
|
||||
|
||||
@property
|
||||
@@ -163,12 +163,12 @@ class QueryBuilder:
|
||||
self.author = args.get("author")
|
||||
|
||||
protocol_version = get_int_or_abort(args.get("protocol_version"))
|
||||
minetest_version = args.get("engine_version")
|
||||
if minetest_version == "":
|
||||
minetest_version = None
|
||||
engine_version = args.get("engine_version")
|
||||
if engine_version == "":
|
||||
engine_version = None
|
||||
|
||||
if protocol_version or minetest_version:
|
||||
self.version = MinetestRelease.get(minetest_version, protocol_version)
|
||||
if protocol_version or engine_version:
|
||||
self.version = LuantiRelease.get(engine_version, protocol_version)
|
||||
else:
|
||||
self.version = None
|
||||
|
||||
|
||||
@@ -283,3 +283,7 @@ blockquote {
|
||||
.form-group {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
input[name="first_name"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,26 @@ h3 {
|
||||
letter-spacing: .05em
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.header-anchor {
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
opacity: 0.25;
|
||||
margin: 0 0 0 0.25em;
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
&:hover .header-anchor {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.badge-notify {
|
||||
background:yellow; /* #00bc8c;*/
|
||||
color: black;
|
||||
|
||||
@@ -92,3 +92,12 @@
|
||||
max-height: 1em;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
.release-notes-body {
|
||||
max-height: 20em;
|
||||
overflow: hidden auto;
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class FlaskCelery(Celery):
|
||||
|
||||
def make_celery(app):
|
||||
celery = FlaskCelery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
|
||||
broker=app.config['CELERY_BROKER_URL'])
|
||||
broker=app.config['CELERY_BROKER_URL'], task_track_started=True)
|
||||
|
||||
celery.init_app(app)
|
||||
return celery
|
||||
|
||||
@@ -57,7 +57,7 @@ def _get_or_create_user(forums_username: str, cache: Optional[dict] = None) -> O
|
||||
def check_forum_account(forums_username, force_replace_pic=False):
|
||||
print("### Checking " + forums_username, file=sys.stderr)
|
||||
try:
|
||||
profile = get_profile("https://forum.minetest.net", forums_username)
|
||||
profile = get_profile("https://forum.luanti.org", forums_username)
|
||||
except OSError as e:
|
||||
print(e, file=sys.stderr)
|
||||
return
|
||||
@@ -88,13 +88,13 @@ def check_forum_account(forums_username, force_replace_pic=False):
|
||||
db.session.commit()
|
||||
|
||||
if pic:
|
||||
pic = urljoin("https://forum.minetest.net/", pic)
|
||||
pic = urljoin("https://forum.luanti.org/", pic)
|
||||
print(f"####### Picture: {pic}", file=sys.stderr)
|
||||
print(f"####### User pp {user.profile_pic}", file=sys.stderr)
|
||||
|
||||
pic_needs_replacing = user.profile_pic is None or user.profile_pic == "" or \
|
||||
user.profile_pic.startswith("https://forum.minetest.net") or force_replace_pic
|
||||
if pic_needs_replacing and pic.startswith("https://forum.minetest.net"):
|
||||
user.profile_pic.startswith("https://forum.luanti.org") or force_replace_pic
|
||||
if pic_needs_replacing and pic.startswith("https://forum.luanti.org"):
|
||||
print(f"####### Queueing", file=sys.stderr)
|
||||
set_profile_picture_from_url.delay(user.username, pic)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
from json import JSONDecodeError
|
||||
from zipfile import ZipFile
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
|
||||
import gitdb
|
||||
from flask import url_for
|
||||
@@ -31,13 +31,13 @@ from sqlalchemy import and_
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
|
||||
from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \
|
||||
MinetestRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
|
||||
LuantiRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
|
||||
PackageGameSupport, PackageTranslation, Language
|
||||
from app.tasks import celery, TaskError
|
||||
from app.utils import random_string, post_bot_message, add_system_notification, add_system_audit_log, \
|
||||
get_games_from_list, add_audit_log
|
||||
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir, get_release_notes
|
||||
from .minetestcheck import build_tree, MinetestCheckError, ContentType, PackageTreeNode
|
||||
from .luanticheck import build_tree, LuantiCheckError, ContentType, PackageTreeNode
|
||||
from .webhooktasks import post_discord_webhook
|
||||
from app import app
|
||||
from app.logic.LogicError import LogicError
|
||||
@@ -51,7 +51,7 @@ def get_meta(urlstr, author):
|
||||
with clone_repo(urlstr, recursive=True) as repo:
|
||||
try:
|
||||
tree = build_tree(repo.working_tree_dir, author=author, repo=urlstr)
|
||||
except MinetestCheckError as err:
|
||||
except LuantiCheckError as err:
|
||||
raise TaskError(str(err))
|
||||
|
||||
result = {"name": tree.name, "type": tree.type.name}
|
||||
@@ -71,8 +71,6 @@ def get_meta(urlstr, author):
|
||||
data = json.loads(f.read())
|
||||
for key, value in data.items():
|
||||
result[key] = value
|
||||
except LogicError as e:
|
||||
raise TaskError(e.message)
|
||||
except JSONDecodeError as e:
|
||||
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
||||
except IOError:
|
||||
@@ -115,7 +113,9 @@ def post_release_check_update(self, release: PackageRelease, path):
|
||||
author=release.package.author.username, name=release.package.name)
|
||||
|
||||
if tree.name is not None and release.package.name != tree.name and tree.type == ContentType.MOD:
|
||||
raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}")
|
||||
raise LuantiCheckError(f"Package name ({release.package.name}) does not match the name of the content in "
|
||||
f"the release ({tree.name}). Either change the package name on ContentDB or the "
|
||||
f"name in the .conf of the content. Then make a new release")
|
||||
|
||||
cache = {}
|
||||
def get_meta_packages(names):
|
||||
@@ -124,6 +124,9 @@ def post_release_check_update(self, release: PackageRelease, path):
|
||||
provides = tree.get_mod_names()
|
||||
|
||||
package = release.package
|
||||
if not package.approved:
|
||||
tree.check_for_legacy_files()
|
||||
|
||||
old_provided_names = set([x.name for x in package.provides])
|
||||
package.provides.clear()
|
||||
|
||||
@@ -164,10 +167,10 @@ def post_release_check_update(self, release: PackageRelease, path):
|
||||
# Raise error on unresolved game dependencies
|
||||
if package.type == PackageType.GAME and len(depends) > 0:
|
||||
deps = ", ".join(depends)
|
||||
raise MinetestCheckError("Game has unresolved hard dependencies: " + deps)
|
||||
raise LuantiCheckError("Game has unresolved hard dependencies: " + deps)
|
||||
|
||||
if package.state != PackageState.APPROVED and tree.find_license_file() is None:
|
||||
raise MinetestCheckError(
|
||||
raise LuantiCheckError(
|
||||
"You need to add a LICENSE.txt/.md or COPYING file to your package. See the 'Copyright Guide' for more info")
|
||||
|
||||
# Add dependencies
|
||||
@@ -182,17 +185,17 @@ def post_release_check_update(self, release: PackageRelease, path):
|
||||
|
||||
# Update min/max
|
||||
if tree.meta.get("min_minetest_version"):
|
||||
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
|
||||
release.min_rel = LuantiRelease.get(tree.meta["min_minetest_version"], None)
|
||||
|
||||
if tree.meta.get("max_minetest_version"):
|
||||
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
|
||||
release.max_rel = LuantiRelease.get(tree.meta["max_minetest_version"], None)
|
||||
|
||||
try:
|
||||
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:
|
||||
data = json.loads(f.read())
|
||||
do_edit_package(package.author, package, False, False, data, "Post release hook")
|
||||
except LogicError as e:
|
||||
raise TaskError(e.message)
|
||||
raise TaskError("Whilst applying .cdb.json: " + e.message)
|
||||
except JSONDecodeError as e:
|
||||
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
||||
except IOError:
|
||||
@@ -231,7 +234,7 @@ def post_release_check_update(self, release: PackageRelease, path):
|
||||
|
||||
return tree
|
||||
|
||||
except (MinetestCheckError, TaskError, LogicError) as err:
|
||||
except (LuantiCheckError, TaskError, LogicError) as err:
|
||||
db.session.rollback()
|
||||
|
||||
error_message = err.value if hasattr(err, "value") else str(err)
|
||||
@@ -268,11 +271,8 @@ def update_translations(package: Package, tree: PackageTreeNode):
|
||||
)
|
||||
conn.execute(stmt)
|
||||
|
||||
raw_translations = tree.get_translations(tree.get("textdomain", tree.name))
|
||||
raw_translations = tree.get_translations(tree.get("textdomain", tree.name), allowed_languages=allowed_languages)
|
||||
for raw_translation in raw_translations:
|
||||
if raw_translation.language not in allowed_languages:
|
||||
continue
|
||||
|
||||
to_update = {
|
||||
"title": raw_translation.entries.get(tree.get("title", package.title)),
|
||||
"short_desc": raw_translation.entries.get(tree.get("description", package.short_desc)),
|
||||
@@ -306,13 +306,16 @@ def _check_zip_file(temp_dir: str, zf: ZipFile) -> bool:
|
||||
|
||||
|
||||
def _safe_extract_zip(temp_dir: str, archive_path: str) -> bool:
|
||||
with ZipFile(archive_path, 'r') as zf:
|
||||
if not _check_zip_file(temp_dir, zf):
|
||||
return False
|
||||
try:
|
||||
with ZipFile(archive_path, 'r') as zf:
|
||||
if not _check_zip_file(temp_dir, zf):
|
||||
return False
|
||||
|
||||
# Extract all
|
||||
for member in zf.infolist():
|
||||
zf.extract(member, temp_dir)
|
||||
# Extract all
|
||||
for member in zf.infolist():
|
||||
zf.extract(member, temp_dir)
|
||||
except BadZipFile as e:
|
||||
raise TaskError(str(e))
|
||||
|
||||
return True
|
||||
|
||||
@@ -334,6 +337,7 @@ def check_zip_release(self, id, path):
|
||||
post_release_check_update(self, release, temp)
|
||||
|
||||
release.task_id = None
|
||||
release.calculate_file_size_bytes()
|
||||
release.approve(release.package.author)
|
||||
db.session.commit()
|
||||
|
||||
@@ -342,16 +346,15 @@ def check_zip_release(self, id, path):
|
||||
def check_all_zip_files():
|
||||
result = []
|
||||
|
||||
with get_temp_dir() as temp:
|
||||
releases = PackageRelease.query.all()
|
||||
for release in releases:
|
||||
with ZipFile(release.file_path, 'r') as zf:
|
||||
if not _check_zip_file(temp, zf):
|
||||
print(f"Unsafe zip file for {release.package.get_id} at {release.file_path}", file=sys.stderr)
|
||||
result.append({
|
||||
"package": release.package.get_id(),
|
||||
"file": release.file_path,
|
||||
})
|
||||
releases = PackageRelease.query.all()
|
||||
for release in releases:
|
||||
with ZipFile(release.file_path, 'r') as zf:
|
||||
if not _check_zip_file("/tmp/example", zf):
|
||||
print(f"Unsafe zip file for {release.package.get_id()} at {release.file_path}", file=sys.stderr)
|
||||
result.append({
|
||||
"package": release.package.get_id(),
|
||||
"file": release.file_path,
|
||||
})
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
@@ -376,7 +379,7 @@ def import_languages(self, id, path):
|
||||
strict=False)
|
||||
update_translations(release.package, tree)
|
||||
db.session.commit()
|
||||
except (MinetestCheckError, TaskError, LogicError) as err:
|
||||
except (LuantiCheckError, TaskError, LogicError) as err:
|
||||
db.session.rollback()
|
||||
|
||||
task_url = url_for('tasks.check', id=self.request.id)
|
||||
@@ -414,6 +417,7 @@ def make_vcs_release(self, id, branch):
|
||||
|
||||
release.url = "/uploads/" + filename
|
||||
release.task_id = None
|
||||
release.calculate_file_size_bytes()
|
||||
release.approve(release.package.author)
|
||||
db.session.commit()
|
||||
|
||||
@@ -476,13 +480,11 @@ def check_update_config_impl(package):
|
||||
if config.last_commit == commit:
|
||||
if tag and config.last_tag != tag:
|
||||
config.last_tag = tag
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
if not config.last_commit:
|
||||
config.last_commit = commit
|
||||
config.last_tag = tag
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
if package.releases.filter_by(commit_hash=commit).count() > 0:
|
||||
@@ -501,8 +503,6 @@ def check_update_config_impl(package):
|
||||
msg = "Created release {} (Git Update Detection)".format(rel.title)
|
||||
add_system_audit_log(AuditSeverity.NORMAL, msg, package.get_url("packages.view"), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
make_vcs_release.apply_async((rel.id, commit), task_id=rel.task_id)
|
||||
|
||||
elif config.outdated_at is None:
|
||||
@@ -529,10 +529,9 @@ def check_update_config_impl(package):
|
||||
|
||||
config.last_commit = commit
|
||||
config.last_tag = tag
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
@celery.task(bind=True, rate_limit="60/m")
|
||||
def check_update_config(self, package_id):
|
||||
package: Package = Package.query.get(package_id)
|
||||
if package is None:
|
||||
@@ -543,6 +542,9 @@ def check_update_config(self, package_id):
|
||||
err = None
|
||||
try:
|
||||
check_update_config_impl(package)
|
||||
|
||||
package.update_config.last_checked_at = datetime.datetime.now()
|
||||
db.session.commit()
|
||||
except GitCommandError as e:
|
||||
# This is needed to stop the backtrace being weird
|
||||
err = e.stderr
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MinetestCheckError(Exception):
|
||||
class LuantiCheckError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
@@ -43,14 +43,14 @@ class ContentType(Enum):
|
||||
|
||||
if self == ContentType.MOD:
|
||||
if not other.is_mod_like():
|
||||
raise MinetestCheckError("Expected a mod or modpack, found " + other.value)
|
||||
raise LuantiCheckError("Expected a mod or modpack, found " + other.value)
|
||||
|
||||
elif self == ContentType.TXP:
|
||||
if other != ContentType.UNKNOWN and other != ContentType.TXP:
|
||||
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
|
||||
raise LuantiCheckError("expected a " + self.value + ", found a " + other.value)
|
||||
|
||||
elif other != self:
|
||||
raise MinetestCheckError("Expected a " + self.value + ", found a " + other.value)
|
||||
raise LuantiCheckError("Expected a " + self.value + ", found a " + other.value)
|
||||
|
||||
|
||||
from .tree import PackageTreeNode, get_base_dir
|
||||
@@ -20,12 +20,12 @@ import re
|
||||
import glob
|
||||
from typing import Optional
|
||||
|
||||
from . import MinetestCheckError, ContentType
|
||||
from . import LuantiCheckError, ContentType
|
||||
from .config import parse_conf
|
||||
from .translation import Translation, parse_tr
|
||||
|
||||
basenamePattern = re.compile("^([a-z0-9_]+)$")
|
||||
licensePattern = re.compile("^(licen[sc]e|copying)(.[^/\n]+)?$", re.IGNORECASE)
|
||||
licensePattern = re.compile("^licen[sc]e[^/.]*(\.(txt|md))?$", re.IGNORECASE)
|
||||
|
||||
DISALLOWED_NAMES = {
|
||||
"core", "minetest", "group", "table", "string", "lua", "luajit", "assert", "debug",
|
||||
@@ -73,10 +73,10 @@ def check_name_list(key: str, value: list[str], relative: str, allow_star: bool
|
||||
if dep == "*" and allow_star:
|
||||
continue
|
||||
elif " " in dep:
|
||||
raise MinetestCheckError(
|
||||
raise LuantiCheckError(
|
||||
f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?")
|
||||
else:
|
||||
raise MinetestCheckError(
|
||||
raise LuantiCheckError(
|
||||
f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.")
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ class PackageTreeNode:
|
||||
children: list
|
||||
type: ContentType
|
||||
strict: bool
|
||||
has_legacy_depends: bool
|
||||
has_legacy_description: bool
|
||||
|
||||
def __init__(self, base_dir: str, relative: str,
|
||||
author: Optional[str] = None,
|
||||
@@ -103,6 +105,8 @@ class PackageTreeNode:
|
||||
self.meta = {}
|
||||
self.children = []
|
||||
self.strict = strict
|
||||
self.has_legacy_depends = False
|
||||
self.has_legacy_description = False
|
||||
|
||||
# Detect type
|
||||
self.type = detect_type(base_dir)
|
||||
@@ -110,14 +114,14 @@ class PackageTreeNode:
|
||||
|
||||
if self.type == ContentType.GAME:
|
||||
if not os.path.isdir(os.path.join(base_dir, "mods")):
|
||||
raise MinetestCheckError("Game at {} does not have a mods/ folder".format(self.relative))
|
||||
raise LuantiCheckError("Game at {} does not have a mods/ folder".format(self.relative))
|
||||
self._add_children_from_mod_dir("mods")
|
||||
elif self.type == ContentType.MOD:
|
||||
if self.name and not basenamePattern.match(self.name):
|
||||
raise MinetestCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
|
||||
raise LuantiCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
|
||||
|
||||
if self.name and self.name in DISALLOWED_NAMES:
|
||||
raise MinetestCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
|
||||
raise LuantiCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
|
||||
|
||||
self._check_dir_casing(["textures", "media", "sounds", "models", "locale"])
|
||||
elif self.type == ContentType.MODPACK:
|
||||
@@ -135,7 +139,7 @@ class PackageTreeNode:
|
||||
for dir in next(os.walk(self.baseDir))[1]:
|
||||
lowercase = dir.lower()
|
||||
if lowercase != dir and lowercase in dirs:
|
||||
raise MinetestCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
|
||||
raise LuantiCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
|
||||
|
||||
def get_readme_path(self):
|
||||
for filename in os.listdir(self.baseDir):
|
||||
@@ -169,12 +173,12 @@ class PackageTreeNode:
|
||||
for key, value in conf.items():
|
||||
result[key] = value
|
||||
except SyntaxError as e:
|
||||
raise MinetestCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
|
||||
raise LuantiCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
if self.strict and "release" in result:
|
||||
raise MinetestCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
|
||||
raise LuantiCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
|
||||
|
||||
# description.txt
|
||||
if "description" not in result:
|
||||
@@ -184,6 +188,11 @@ class PackageTreeNode:
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
if os.path.isfile(self.baseDir + "/depends.txt"):
|
||||
self.has_legacy_depends = True
|
||||
if os.path.isfile(self.baseDir + "/description.txt"):
|
||||
self.has_legacy_description = True
|
||||
|
||||
# Read dependencies
|
||||
if "depends" in result or "optional_depends" in result:
|
||||
result["depends"] = get_csv_line(result.get("depends"))
|
||||
@@ -235,9 +244,12 @@ class PackageTreeNode:
|
||||
# Calculate short description
|
||||
if "description" in result:
|
||||
desc = result["description"]
|
||||
idx = desc.find(".") + 1
|
||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:cutIdx]
|
||||
if len(desc) > 200:
|
||||
idx = desc.find(".") + 1
|
||||
idx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:idx]
|
||||
else:
|
||||
result["short_description"] = desc
|
||||
|
||||
if "name" in result:
|
||||
self.name = result["name"]
|
||||
@@ -257,11 +269,11 @@ class PackageTreeNode:
|
||||
if not entry.startswith('.') and os.path.isdir(path):
|
||||
child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict)
|
||||
if not child.type.is_mod_like():
|
||||
raise MinetestCheckError("Expecting mod or modpack, found {} at {} inside {}" \
|
||||
raise LuantiCheckError("Expecting mod or modpack, found {} at {} inside {}" \
|
||||
.format(child.type.value, child.relative, self.type.value))
|
||||
|
||||
if child.name is None:
|
||||
raise MinetestCheckError("Missing base name for mod at {}".format(self.relative))
|
||||
raise LuantiCheckError("Missing base name for mod at {}".format(self.relative))
|
||||
|
||||
self.children.append(child)
|
||||
|
||||
@@ -301,6 +313,16 @@ class PackageTreeNode:
|
||||
def get(self, key: str, default=None):
|
||||
return self.meta.get(key, default)
|
||||
|
||||
def check_for_legacy_files(self):
|
||||
if self.has_legacy_depends:
|
||||
raise LuantiCheckError("Found depends.txt at {}. Delete this file and use depends in mod.conf instead" \
|
||||
.format(self.relative))
|
||||
if self.has_legacy_description:
|
||||
raise LuantiCheckError("Found description.txt at {}. Delete this file and use description in {} instead" \
|
||||
.format(self.relative, self.get_meta_file_name()))
|
||||
for child in self.children:
|
||||
child.check_for_legacy_files()
|
||||
|
||||
def validate(self):
|
||||
for child in self.children:
|
||||
child.validate()
|
||||
@@ -313,14 +335,19 @@ class PackageTreeNode:
|
||||
|
||||
return ret
|
||||
|
||||
def get_translations(self, textdomain: str) -> list[Translation]:
|
||||
def get_translations(self, textdomain: str, allowed_languages: set[str]) -> list[Translation]:
|
||||
ret = []
|
||||
|
||||
for name in glob.glob(f"{self.baseDir}/**/locale/{textdomain}.*.tr", recursive=True):
|
||||
parts = os.path.basename(name).split(".")
|
||||
lang = parts[-2]
|
||||
if lang not in allowed_languages:
|
||||
continue
|
||||
|
||||
try:
|
||||
ret.append(parse_tr(name))
|
||||
except SyntaxError as e:
|
||||
relative_path = os.path.join(self.relative, os.path.relpath(name, self.baseDir))
|
||||
raise MinetestCheckError(f"Syntax error whilst reading {relative_path}: {e}")
|
||||
raise LuantiCheckError(f"Syntax error whilst reading {relative_path}: {e}")
|
||||
|
||||
return ret
|
||||
@@ -19,15 +19,16 @@ import random
|
||||
import re
|
||||
import sys
|
||||
from time import sleep
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
from app import app
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from app.markdown import get_links, render_markdown
|
||||
from app.models import Package, db, PackageState, AuditLogEntry, AuditSeverity
|
||||
from app.models import db, Package, PackageState, PackageRelease, PackageScreenshot, AuditLogEntry, AuditSeverity
|
||||
from app.tasks import celery, TaskError
|
||||
from app.utils import post_bot_message, post_to_approval_thread, get_system_user, add_audit_log
|
||||
|
||||
@@ -44,7 +45,7 @@ def update_package_scores():
|
||||
|
||||
|
||||
def desc_contains(desc: str, search_str: str):
|
||||
if search_str.startswith("https://forum.minetest.net/viewtopic.php?%t="):
|
||||
if search_str.startswith("https://forum.luanti.org/viewtopic.php?%t="):
|
||||
reg = re.compile(search_str.replace(".", "\\.").replace("/", "\\/").replace("?", "\\?").replace("%", ".*"))
|
||||
return reg.search(desc)
|
||||
else:
|
||||
@@ -57,7 +58,7 @@ def notify_about_git_forum_links():
|
||||
.filter(Package.repo.is_not(None), Package.state == PackageState.APPROVED).all()]
|
||||
for pair in db.session.query(Package, Package.forums) \
|
||||
.filter(Package.forums.is_not(None), Package.state == PackageState.APPROVED).all():
|
||||
package_links.append((pair[0], f"https://forum.minetest.net/viewtopic.php?%t={pair[1]}"))
|
||||
package_links.append((pair[0], f"https://forum.luanti.org/viewtopic.php?%t={pair[1]}"))
|
||||
|
||||
clauses = [and_(Package.id != pair[0].id, Package.desc.ilike(f"%{pair[1]}%")) for pair in package_links]
|
||||
packages = Package.query.filter(Package.desc != "", Package.desc.is_not(None), Package.state == PackageState.APPROVED, or_(*clauses)).all()
|
||||
@@ -110,12 +111,15 @@ def clear_removed_packages(all_packages: bool):
|
||||
def _url_exists(url: str) -> str:
|
||||
try:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; ContentDB link checker; +https://content.minetest.net/)",
|
||||
"User-Agent": "Mozilla/5.0 (compatible; ContentDB link checker; +https://content.luanti.org/)",
|
||||
}
|
||||
with requests.get(url, stream=True, headers=headers, timeout=10) as response:
|
||||
response.raise_for_status()
|
||||
return ""
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 403:
|
||||
return ""
|
||||
|
||||
print(f" - [{e.response.status_code}] <{url}>", file=sys.stderr)
|
||||
return str(e.response.status_code)
|
||||
except requests.exceptions.ConnectionError:
|
||||
@@ -125,6 +129,9 @@ def _url_exists(url: str) -> str:
|
||||
|
||||
|
||||
def _check_for_dead_links(package: Package) -> dict[str, str]:
|
||||
ignored_urls = set(app.config.get("LINK_CHECKER_IGNORED_URLS", ""))
|
||||
|
||||
base_url = package.get_url("packages.view", absolute=True)
|
||||
links: set[Optional[str]] = {
|
||||
package.repo,
|
||||
package.website,
|
||||
@@ -136,7 +143,7 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
|
||||
}
|
||||
|
||||
if package.desc:
|
||||
links.update(get_links(render_markdown(package.desc), package.get_url("packages.view", absolute=True)))
|
||||
links.update(get_links(render_markdown(package.desc)))
|
||||
|
||||
print(f"Checking {package.title} ({len(links)} links) for broken links", file=sys.stderr)
|
||||
|
||||
@@ -146,11 +153,15 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
|
||||
if link is None:
|
||||
continue
|
||||
|
||||
url = urlparse(link)
|
||||
abs_link = urljoin(base_url, link)
|
||||
url = urlparse(abs_link)
|
||||
if url.scheme != "http" and url.scheme != "https":
|
||||
continue
|
||||
|
||||
res = _url_exists(link)
|
||||
if url.hostname in ignored_urls:
|
||||
continue
|
||||
|
||||
res = _url_exists(abs_link)
|
||||
if res != "":
|
||||
bad_urls[link] = res
|
||||
|
||||
@@ -180,7 +191,7 @@ def check_package_on_submit(package_id: int):
|
||||
|
||||
msg = _check_package(package)
|
||||
if msg:
|
||||
marked = f"Marked {package.title} as Changed Needed"
|
||||
marked = f"Marked {package.title} as {PackageState.CHANGES_NEEDED.value}"
|
||||
|
||||
system_user = get_system_user()
|
||||
post_to_approval_thread(package, system_user, marked, is_status_update=True, create_thread=True)
|
||||
@@ -200,3 +211,34 @@ def check_package_for_broken_links(package_id: int):
|
||||
if msg:
|
||||
post_bot_message(package, "Broken links", msg)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def update_file_size_bytes(self):
|
||||
releases = PackageRelease.query.filter_by(file_size_bytes=0).all()
|
||||
screenshots = PackageScreenshot.query.filter_by(file_size_bytes=0).all()
|
||||
total = len(releases) + len(screenshots)
|
||||
self.update_state(state="PROGRESS", meta={
|
||||
"current": 0,
|
||||
"total": total,
|
||||
})
|
||||
|
||||
for i, release in enumerate(releases):
|
||||
release.calculate_file_size_bytes()
|
||||
|
||||
if i % 100 == 0:
|
||||
self.update_state(state="PROGRESS", meta={
|
||||
"current": i + 1,
|
||||
"total": total,
|
||||
})
|
||||
|
||||
for i, ss in enumerate(screenshots):
|
||||
ss.calculate_file_size_bytes()
|
||||
|
||||
if i % 100 == 0:
|
||||
self.update_state(state="PROGRESS", meta={
|
||||
"current": i + len(releases) + 1,
|
||||
"total": total,
|
||||
})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -20,7 +20,7 @@ import os
|
||||
import sys
|
||||
|
||||
from flask import url_for
|
||||
from sqlalchemy import or_, and_
|
||||
from sqlalchemy import or_, and_, not_, func
|
||||
|
||||
from app import app
|
||||
from app.models import User, db, UserRank, ThreadReply, Package, NotificationType
|
||||
@@ -149,3 +149,37 @@ def import_github_user_ids():
|
||||
db.session.commit()
|
||||
|
||||
print(f"Updated {count} users", file=sys.stderr)
|
||||
|
||||
|
||||
@celery.task()
|
||||
def do_delete_likely_spammers():
|
||||
query = (User.query.filter(
|
||||
and_(
|
||||
User.rank == UserRank.NEW_MEMBER,
|
||||
or_(
|
||||
func.replace(User.website_url, ".", "").regexp_match(
|
||||
func.concat("https?://[^/]*", User.username, ".*")),
|
||||
),
|
||||
or_(
|
||||
User.website_url.ilike("%bet%"),
|
||||
User.website_url.ilike("%win%"),
|
||||
User.website_url.ilike("%88%"),
|
||||
User.website_url.ilike("%luck%"),
|
||||
User.website_url.ilike("%sport%"),
|
||||
User.website_url.ilike("%lottery%"),
|
||||
User.website_url.ilike("%casino%"),
|
||||
User.website_url.ilike("%vip%"),
|
||||
User.website_url.ilike("%assignment%"),
|
||||
),
|
||||
~User.packages.any(),
|
||||
~User.replies.any(),
|
||||
~User.reports.any(),
|
||||
not_(or_(
|
||||
User.website_url.ilike("%.github.io%"),
|
||||
User.website_url.ilike("%.neocities.org%"),
|
||||
)),
|
||||
)))
|
||||
|
||||
for user in query.all():
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
@@ -25,10 +25,13 @@ from app.tasks import celery
|
||||
|
||||
@celery.task()
|
||||
def post_discord_webhook(username: Optional[str], content: str, is_queue: bool, title: Optional[str] = None, description: Optional[str] = None, thumbnail: Optional[str] = None):
|
||||
discord_url = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
|
||||
if discord_url is None:
|
||||
discord_urls = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
|
||||
if discord_urls is None:
|
||||
return
|
||||
|
||||
if isinstance(discord_urls, str):
|
||||
discord_urls = [discord_urls]
|
||||
|
||||
json = {
|
||||
"content": content[0:2000],
|
||||
}
|
||||
@@ -52,7 +55,8 @@ def post_discord_webhook(username: Optional[str], content: str, is_queue: bool,
|
||||
|
||||
json["embeds"] = [embed]
|
||||
|
||||
res = requests.post(discord_url, json=json, headers={"Accept": "application/json"})
|
||||
if not res.ok:
|
||||
raise Exception(f"Failed to submit Discord webhook {res.json}")
|
||||
res.raise_for_status()
|
||||
for url in discord_urls:
|
||||
res = requests.post(url, json=json, headers={"Accept": "application/json"})
|
||||
if not res.ok:
|
||||
raise Exception(f"Failed to submit Discord webhook {res.json}")
|
||||
res.raise_for_status()
|
||||
|
||||
@@ -15,45 +15,70 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE
|
||||
from typing import Optional
|
||||
import sys
|
||||
from subprocess import Popen, PIPE, TimeoutExpired
|
||||
from typing import Optional, List
|
||||
|
||||
from app.models import Package, PackageState, PackageRelease
|
||||
from app.tasks import celery
|
||||
|
||||
|
||||
@celery.task()
|
||||
def search_in_releases(query: str, file_filter: str):
|
||||
packages = list(Package.query.filter(Package.state == PackageState.APPROVED).all())
|
||||
running = []
|
||||
@celery.task(bind=True)
|
||||
def search_in_releases(self, query: str, file_filter: str, types: List[str]):
|
||||
pkg_query = Package.query.filter(Package.state == PackageState.APPROVED)
|
||||
if len(types) > 0:
|
||||
pkg_query = pkg_query.filter(Package.type.in_(types))
|
||||
|
||||
packages = list(pkg_query.all())
|
||||
results = []
|
||||
|
||||
while len(packages) > 0 or len(running) > 0:
|
||||
# Check running
|
||||
for i in range(len(running) - 1, -1, -1):
|
||||
package: Package = running[i][0]
|
||||
handle: subprocess.Popen[str] = running[i][1]
|
||||
total = len(packages)
|
||||
self.update_state(state="PROGRESS", meta={"current": 0, "total": total})
|
||||
|
||||
while len(packages) > 0:
|
||||
package = packages.pop()
|
||||
release: Optional[PackageRelease] = package.get_download_release()
|
||||
if release:
|
||||
print(f"[Zipgrep] Checking {package.name}", file=sys.stderr)
|
||||
self.update_state(state="PROGRESS", meta={
|
||||
"current": total - len(packages),
|
||||
"total": total,
|
||||
"running": [package.as_key_dict()],
|
||||
})
|
||||
|
||||
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
|
||||
|
||||
try:
|
||||
handle.wait(timeout=45)
|
||||
except TimeoutExpired:
|
||||
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
|
||||
handle.kill()
|
||||
results.append({
|
||||
"package": package.as_key_dict(),
|
||||
"lines": "Error: timeout",
|
||||
})
|
||||
continue
|
||||
|
||||
exit_code = handle.poll()
|
||||
if exit_code is None:
|
||||
continue
|
||||
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
|
||||
handle.kill()
|
||||
results.append({
|
||||
"package": package.as_key_dict(),
|
||||
"lines": "Error: timeout",
|
||||
})
|
||||
elif exit_code == 0:
|
||||
print(f"[Zipgrep] Success for {package.name}", file=sys.stderr)
|
||||
results.append({
|
||||
"package": package.as_key_dict(),
|
||||
"lines": handle.stdout.read(),
|
||||
})
|
||||
|
||||
del running[i]
|
||||
|
||||
# Create new
|
||||
while len(running) < 1 and len(packages) > 0:
|
||||
package = packages.pop()
|
||||
release: Optional[PackageRelease] = package.get_download_release()
|
||||
if release:
|
||||
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
|
||||
running.append([package, handle])
|
||||
|
||||
if len(running) > 0:
|
||||
running[0][1].wait()
|
||||
elif exit_code != 1:
|
||||
print(f"[Zipgrep] Error {exit_code} for {package.name}", file=sys.stderr)
|
||||
results.append({
|
||||
"package": package.as_key_dict(),
|
||||
"lines": f"Error: exit {exit_code}",
|
||||
})
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
|
||||
@@ -23,10 +23,10 @@ from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
|
||||
from . import app, utils
|
||||
from .markdown import get_headings
|
||||
from app.markdown import get_headings
|
||||
from .models import Permission, Package, PackageState, PackageRelease
|
||||
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current
|
||||
from .utils.minetest_hypertext import normalize_whitespace as do_normalize_whitespace
|
||||
from .utils.luanti_hypertext import normalize_whitespace as do_normalize_whitespace
|
||||
|
||||
|
||||
@app.context_processor
|
||||
|
||||
@@ -7,6 +7,14 @@ Audit Log
|
||||
{% block content %}
|
||||
<h1>Audit Log</h1>
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||
<form method="GET" action="">
|
||||
{{ render_field(form.username) }}
|
||||
{{ render_field(form.q) }}
|
||||
{{ render_field(form.url) }}
|
||||
{{ render_submit_field(form.submit) }}
|
||||
</form>
|
||||
|
||||
{% from "macros/pagination.html" import render_pagination %}
|
||||
{% from "macros/audit_log.html" import render_audit_log %}
|
||||
|
||||
|
||||
@@ -13,14 +13,20 @@
|
||||
<div class="list-group">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
User list
|
||||
{{ _("User list") }}
|
||||
</a>
|
||||
{% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
|
||||
{% if current_user.rank.at_least(current_user.rank.APPROVER) %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.audit') }}">
|
||||
<i class="fas fa-user-clock me-2"></i>
|
||||
{{ _("Audit Log") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.rank.at_least(current_user.rank.EDITOR) %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('report.list_all') }}">
|
||||
<i class="fas fa-user-clock me-2"></i>
|
||||
Reports
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h3>Packages</h3>
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
{{ _("Package") }}
|
||||
</div>
|
||||
<div class="col-2 text-center">
|
||||
Latest release / MB
|
||||
Latest release (MB)
|
||||
</div>
|
||||
<div class="col-2 text-center">
|
||||
Releases / MB
|
||||
Releases (MB)
|
||||
</div>
|
||||
<div class="col-2 text-center">
|
||||
Screenshots / MB
|
||||
Screenshots (MB)
|
||||
</div>
|
||||
<div class="col-2 text-center">
|
||||
Total / MB
|
||||
Total (MB)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% if version %}
|
||||
Edit {{ version.name }}
|
||||
{% else %}
|
||||
New Minetest Version
|
||||
New Luanti Version
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ _("Minetest Versions") }}
|
||||
{{ _("Luanti Versions") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_version') }}">{{ _("New Version") }}</a>
|
||||
|
||||
<h1>{{ _("Minetest Versions") }}</h1>
|
||||
<h1>{{ _("Luanti Versions") }}</h1>
|
||||
|
||||
<div class="list-group">
|
||||
{% for v in versions %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{%- endif %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4">
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=54">
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=59">
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||
|
||||
{% if noindex -%}
|
||||
@@ -252,14 +252,14 @@
|
||||
|
||||
<footer class="my-5 pt-5">
|
||||
<p class="pt-3 mb-1">
|
||||
ContentDB © 2018-23 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
|
||||
ContentDB © 2018-24 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
|
||||
</p>
|
||||
|
||||
<ul class="list-inline my-1">
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='about') }}">{{ _("About") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/contact_us') }}">{{ _("Contact Us") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='rules') }}">{{ _("Rules") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='terms') }}">{{ _("Terms of Service") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}#contentdb">{{ _("Donate") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/api') }}">{{ _("API") }}</a></li>
|
||||
@@ -274,7 +274,7 @@
|
||||
<li class="list-inline-item"><a href="{{ url_for('collections.list_all') }}">{{ _("Collections") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}">{{ _("Support Creators") }}</a></li>
|
||||
<li class="list-inline-item"><a href="{{ url_for('translate.translate') }}">{{ _("Translate Packages") }}</a></li>
|
||||
<li class="list-inline-item"><a href="https://github.com/minetest/contentdb">{{ _("Source Code") }}</a></li>
|
||||
<li class="list-inline-item"><a href="https://github.com/luanti-org/contentdb">{{ _("Source Code") }}</a></li>
|
||||
</ul>
|
||||
|
||||
<form method="POST" action="{{ url_for('set_nonfree') }}" class="my-3">
|
||||
@@ -285,9 +285,11 @@
|
||||
<input type="submit" class="btn btn-sm btn-secondary" value="{{ _('Hide non-free packages') }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
<p class="text-warning">
|
||||
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
|
||||
</p>
|
||||
{% if false %}
|
||||
<p class="text-warning">
|
||||
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if debug %}
|
||||
<p style="color: red">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{{ _("Welcome to the best place to find Minetest mods, games, and texture packs") }}
|
||||
{{ _("Welcome to the best place to find Luanti mods, games, and texture packs") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scriptextra %}
|
||||
@@ -13,10 +13,10 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"url": "https://content.minetest.net/",
|
||||
"url": "https://content.luanti.org/",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "https://content.minetest.net/packages?q={search_term_string}",
|
||||
"target": "https://content.luanti.org/packages?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
@@ -39,24 +39,25 @@
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
{% for package in spotlight_pkgs %}
|
||||
{% set meta = package.get_translated(load_desc=False) %}
|
||||
{% set cover_image = package.get_cover_image_url() %}
|
||||
{% set tags = package.tags | sort(attribute="views", reverse=True) %}
|
||||
<div class="carousel-item {% if loop.index == 1 %}active{% endif %}">
|
||||
<a href="{{ package.get_url('packages.view') }}">
|
||||
<div class="ratio ratio-16x9">
|
||||
<img src="{{ cover_image }}"
|
||||
alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}">
|
||||
alt="{{ _('%(title)s by %(author)s', title=meta.title, author=package.author.display_name) }}">
|
||||
</div>
|
||||
<div class="carousel-caption text-shadow">
|
||||
<h3 class="mt-0 mb-3">
|
||||
{% if package.author %}
|
||||
{{ _('<strong>%(title)s</strong> by %(author)s', title=package.title, author=package.author.display_name) }}
|
||||
{{ _('<strong>%(title)s</strong> by %(author)s', title=meta.title, author=package.author.display_name) }}
|
||||
{% else %}
|
||||
<strong>{{ package.title }}</strong>
|
||||
<strong>{{ meta.title }}</strong>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p>
|
||||
{{ package.short_desc }}
|
||||
{{ meta.short_desc }}
|
||||
</p>
|
||||
{% if package.author %}
|
||||
<div class="d-none d-md-block">
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
|
||||
<h3 class="mt-5">{{ _("Downloads by Reason") }}</h3>
|
||||
<ul>
|
||||
<li>{{ _("<b>New Install</b>: the user clicked [Install] inside of Minetest.") }}</li>
|
||||
<li>{{ _("<b>New Install</b>: the user clicked [Install] inside of Luanti.") }}</li>
|
||||
<li>{{ _("<b>Dependency</b>: was installed automatically to fulfill a dependency.") }}</li>
|
||||
<li>{{ _("<b>Update</b>: download was to update the package.") }}</li>
|
||||
<li>{{ _("<b>Other / Unknown</b>: downloaded by a web browser or an outdated Minetest version (before 5.5).") }}</li>
|
||||
<li>{{ _("<b>Other / Unknown</b>: downloaded by a web browser or an outdated Luanti version (before 5.5).") }}</li>
|
||||
</ul>
|
||||
<p class="text-muted">
|
||||
{{ _("This is a stacked area graph. For total downloads, look at the combined height.") }}
|
||||
|
||||
@@ -176,11 +176,6 @@
|
||||
<input class="btn btn-primary" name="btn_submit" type="submit" value="Comment" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if thread.private %}
|
||||
<p class="text-muted card-body my-0 pt-0">
|
||||
{{ _("You can add someone to a private thread by writing @username.") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[{{ topic.type.text }}]
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||
<a href="https://forum.luanti.org/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||
{% if topic.wip %}[{{ _("WIP") }}]{% endif %}
|
||||
</td>
|
||||
{% if show_author %}
|
||||
@@ -42,7 +42,7 @@
|
||||
{% macro render_topics(topics, current_user) -%}
|
||||
<div class="list-group">
|
||||
{% for topic in topics %}
|
||||
<a class="list-group-item list-group-item-action" href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">
|
||||
<a class="list-group-item list-group-item-action" href="https://forum.luanti.org/viewtopic.php?t={{ topic.topic_id}}">
|
||||
<span class="float-end text-muted">
|
||||
{{ topic.created_at | date }}
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% for t in similar_topics %}
|
||||
<li>
|
||||
[{{ t.type.text }}]
|
||||
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
|
||||
<a href="https://forum.luanti.org/viewtopic.php?t={{ t.topic_id }}">
|
||||
{{ _("%(title)s by %(display_name)s", title=t.title, display_name=t.author.display_name) }}
|
||||
</a>
|
||||
{% if t.wip %}[{{ _("WIP") }}]{% endif %}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ render_field(form.title) }}
|
||||
{{ render_field(form.description, hint=_("Shown to users when you request access to their account")) }}
|
||||
{{ render_field(form.title, hint=_("Titles must be globally unique. For example, what's the name of your application?")) }}
|
||||
{{ render_field(form.description, hint=_("Shown to users when you request access to their account. For example, what does your application do?")) }}
|
||||
{{ render_field(form.redirect_url) }}
|
||||
{{ render_field(form.app_type, hint=_("Where will you store your client_secret?")) }}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user