Compare commits

..

1 Commits

Author SHA1 Message Date
rubenwardy
2a84ec5bad Add broken test for game support issue 2024-07-09 00:12:49 +01:00
146 changed files with 44570 additions and 60731 deletions

2
.github/SECURITY.md vendored
View File

@@ -2,7 +2,7 @@
## Supported Versions
We only support the latest production version, deployed to <https://content.luanti.org>.
We only support the latest production version, deployed to <https://content.minetest.net>.
This is usually the latest `master` commit.
## Reporting a Vulnerability

View File

@@ -6,8 +6,6 @@ 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/* .

View File

@@ -1,20 +1,16 @@
FROM python:3.10.11-alpine
FROM python:3.10.11
RUN addgroup --gid 5123 cdb && \
adduser --uid 5123 -S cdb -G cdb
RUN groupadd -g 5123 cdb && \
useradd -r -u 5123 -g cdb 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 && \
pip install gunicorn
RUN pip install -r requirements.lock.txt
RUN pip install gunicorn
COPY utils utils
COPY config.cfg config.cfg

View File

@@ -21,12 +21,13 @@ 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, render_markdown
from app.markdown import init_markdown, MARKDOWN_EXTENSIONS, MARKDOWN_EXTENSION_CONFIG
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
@@ -66,18 +67,19 @@ app = Flask(__name__, static_folder="public/static")
def my_flatpage_renderer(text):
# Render with jinja first
prerendered_body = render_template_string(text)
return render_markdown(prerendered_body, clean=False)
return pygmented_markdown(prerendered_body, flatpages=pages)
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",
@@ -88,7 +90,6 @@ app.config["LANGUAGES"] = {
"ru": "русский язык",
"sk": "Slovenčina",
"sv": "Svenska",
"ta": "தமிழ்",
"tr": "Türkçe",
"uk": "Українська",
"vi": "tiếng Việt",

View File

@@ -1,252 +1,248 @@
# THIS FILE IS AUTOGENERATED: utils/extract_translations.py
from flask_babel import pgettext
from flask_babel import gettext
# 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: 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", "Less than 16px")
# 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 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: 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: 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 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 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 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: 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 drugs
pgettext("content_warnings", "Drugs")
# NOTE: content_warnings: description for drugs
pgettext("content_warnings", "Contains recreational drugs other than alcohol or tobacco")

View File

@@ -113,10 +113,9 @@ 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)
if data["long_description"] is not None:
html = render_markdown(data["long_description"])
data["long_description"] = html_to_minetest(html, page_url, formspec_version, include_images)
data["long_description"] = html_to_minetest(html, page_url, formspec_version, include_images)
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
@@ -151,7 +150,7 @@ 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 if package.desc else "")
html = render_markdown(package.desc)
page_url = package.get_url("packages.view", absolute=True)
return jsonify(html_to_minetest(html, page_url, formspec_version, include_images))
@@ -570,14 +569,14 @@ def package_scores():
@cors_allowed
@cached(60*60)
def tags():
return jsonify([tag.as_dict() for tag in Tag.query.order_by(db.asc(Tag.name)).all()])
return jsonify([tag.as_dict() for tag in Tag.query.all() ])
@bp.route("/api/content_warnings/")
@cors_allowed
@cached(60*60)
def content_warnings():
return jsonify([warning.as_dict() for warning in ContentWarning.query.order_by(db.asc(ContentWarning.name)).all() ])
return jsonify([warning.as_dict() for warning in ContentWarning.query.all() ])
@bp.route("/api/licenses/")
@@ -630,6 +629,24 @@ 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():

View File

@@ -112,9 +112,9 @@ def api_edit_package(token: APIToken, package: Package, data: dict, reason: str
reason += ", token=" + token.name
was_modified = guard(do_edit_package)(token.owner, package, False, False, data, reason)
package = guard(do_edit_package)(token.owner, package, False, False, data, reason)
return jsonify({
"success": True,
"package": package.as_dict(current_app.config["BASE_URL"]),
"was_modified": was_modified,
"package": package.as_dict(current_app.config["BASE_URL"])
})

View File

@@ -17,7 +17,7 @@
import re
import typing
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for, jsonify
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for
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, should_return_json
from app.utils import nonempty_or_none, normalize_line_endings
from app.utils.models import is_package_page, add_audit_log, create_session
bp = Blueprint("collections", __name__)
@@ -70,10 +70,7 @@ 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)]
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)
return render_template("collections/view.html", collection=collection, items=items)
class CollectionForm(FlaskForm):

View File

@@ -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 Luanti mods, games, and texture packs"),
"home_page_url": "https://content.luanti.org/",
"description": gettext("Welcome to the best place to find Minetest mods, games, and texture packs"),
"home_page_url": "https://content.minetest.net/",
"feed_url": feed_url,
"icon": "https://content.luanti.org/favicon-128.png",
"icon": "https://content.minetest.net/favicon-128.png",
"expired": False,
"items": items,
}

View File

@@ -194,10 +194,6 @@ 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
@@ -205,7 +201,6 @@ 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"

View File

@@ -74,7 +74,7 @@ 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("Luanti Version"),
engine_version = QuerySelectField(lazy_gettext("Minetest Version"),
query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)),
allow_blank=True, blank_value="",
get_pk=lambda a: a.value, get_label=lambda a: a.name)

View File

@@ -266,7 +266,6 @@ 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>" +
@@ -459,7 +458,6 @@ 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")
@@ -572,7 +570,7 @@ def edit_maintainers(package):
for user in users:
if not user in package.maintainers:
if thread and user not in thread.watchers:
if thread:
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)

View File

@@ -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 Luanti Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest 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 Luanti Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest 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 Luanti Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest 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 Luanti Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest 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"))
@@ -127,8 +127,7 @@ 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():
user_agent = request.headers.get("User-Agent") or ""
is_minetest = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
is_minetest = (request.headers.get("User-Agent") or "").startswith("Minetest")
reason = request.args.get("reason")
PackageDailyStats.update(package, is_minetest, reason)
@@ -215,10 +214,10 @@ def edit_release(package, id):
class BulkReleaseForm(FlaskForm):
set_min = BooleanField(lazy_gettext("Set Min"))
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest 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 Luanti Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest 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"))

View File

@@ -47,9 +47,6 @@ def report():
url = abs_url_samesite(url)
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
if form and request.method == "GET":
form.message.data = request.args.get("message", "")
if form and form.validate_on_submit():
if current_user.is_authenticated:
user_info = f"{current_user.username}"

View File

@@ -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
from wtforms import StringField, TextAreaField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length
from app.utils import get_int_or_abort
@@ -281,6 +281,7 @@ 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"))
@@ -295,11 +296,14 @@ 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
is_private_thread = is_review_thread
allow_private_change = not is_review_thread
if is_review_thread:
def_is_private = True
# Check that user can make the thread
if package and not package.check_perm(current_user, Permission.CREATE_THREAD):
@@ -322,6 +326,7 @@ 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
@@ -332,7 +337,7 @@ def new():
thread = Thread()
thread.author = current_user
thread.title = form.title.data
thread.private = is_private_thread
thread.private = form.private.data if allow_private_change else def_is_private
thread.package = package
db.session.add(thread)
@@ -362,8 +367,7 @@ def new():
add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.get_view_url(), thread.package)
if mentioned not in thread.watchers:
thread.watchers.append(mentioned)
thread.watchers.append(mentioned)
notif_msg = "New thread '{}'".format(thread.title)
if package is not None:
@@ -380,7 +384,7 @@ def new():
return redirect(thread.get_view_url())
return render_template("threads/new.html", form=form, package=package)
return render_template("threads/new.html", form=form, allow_private_change=allow_private_change, package=package)
@bp.route("/users/<username>/comments/")

View File

@@ -25,11 +25,7 @@ bp = Blueprint("thumbnails", __name__)
ALLOWED_RESOLUTIONS = [(100, 67), (270, 180), (350, 233), (1100, 520)]
ALLOWED_MIMETYPES = {
"png": "image/png",
"webp": "image/webp",
"jpg": "image/jpeg",
}
ALLOWED_EXTENSIONS = {"png", "webp", "jpg"}
def mkdir(path):
@@ -80,10 +76,10 @@ def find_source_file(img):
period = source_filepath.rfind(".")
start = source_filepath[:period]
ext = source_filepath[period + 1:]
if ext not in ALLOWED_MIMETYPES:
if ext not in ALLOWED_EXTENSIONS:
abort(404)
for other_ext in ALLOWED_MIMETYPES.keys():
for other_ext in ALLOWED_EXTENSIONS:
other_path = f"{start}.{other_ext}"
if ext != other_ext and os.path.isfile(other_path):
return other_path
@@ -91,15 +87,6 @@ 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:
@@ -117,7 +104,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, mimetype=get_mimetype(cache_filepath))
res = send_file(cache_filepath)
res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res

View File

@@ -104,6 +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()])
submit = SubmitField(lazy_gettext("Register"))

View File

@@ -77,7 +77,7 @@ def claim_forums():
# Get signature
try:
profile = get_profile("https://forum.luanti.org", username)
profile = get_profile("https://forum.minetest.net", username)
sig = profile.signature if profile else None
except IOError as e:
if hasattr(e, 'message'):

View File

@@ -28,6 +28,7 @@ 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
@@ -97,7 +98,6 @@ 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()

View File

@@ -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, SelectMultipleField
from wtforms.validators import InputRequired, Length, Optional
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import InputRequired, Length
from app.tasks import celery
from app.utils import rank_required
bp = Blueprint("zipgrep", __name__)
from app.models import UserRank, Package, PackageType
from app.models import UserRank, Package
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")
type = SelectMultipleField(lazy_gettext("Type"), [Optional()],
choices=PackageType.choices(), coerce=PackageType.coerce)
remember_me = BooleanField(lazy_gettext("Remember me"), default=True)
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, [x.name for x in form.type.data]), task_id=task_id)
search_in_releases.apply_async((form.query.data, form.file_filter.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))

View File

@@ -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 Luanti**.
and texture packs for Minetest**.
## How do I learn how to make mods and games for Luanti?
## How do I learn how to make mods and games for Minetest?
You should read
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest.
<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
Luanti and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
Minetest 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.

View File

@@ -4,7 +4,7 @@ toc: False
## Rules
* [Terms of Service](/terms/)
* [Rules](/rules/)
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
## General Help

View File

@@ -3,7 +3,7 @@ title: API
## Resources
* [How the Luanti client uses the API](https://github.com/minetest/contentdb/blob/master/docs/minetest_client.md)
* [How the Minetest client uses the API](https://github.com/minetest/contentdb/blob/master/docs/minetest_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.luanti.org/api/whoami/ \
curl https://content.minetest.net/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.luanti.org/api/delete-token/ \
# Logout
curl -X DELETE https://content.minetest.net/api/delete-token/ \
-H "Authorization: Bearer YOURTOKEN"
```
@@ -78,12 +78,9 @@ curl -X DELETE https://content.luanti.org/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 object with any of these keys (all are optional, null to delete Nullables):
* JSON dictionary with any of these keys (all are optional, null to delete Nullables):
* `type`: One of `GAME`, `MOD`, `TXP`.
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
@@ -102,13 +99,9 @@ curl -X DELETE https://content.luanti.org/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,
* Returns a JSON object with:
* `success`
* `package`: updated package
* `was_modified`: bool, whether anything changed
* `game_support`: Array of game support information objects. Not currently documented, as subject to change.
* GET `/api/packages/<username>/<name>/for-client/`
* Similar to the read endpoint, but optimised for the Luanti client
* Similar to the read endpoint, but optimised for the Minetest client
* `long_description` is given as a hypertext object, see `/hypertext/` below.
* `info_hypertext` is the info sidebar as a hypertext object.
* Query arguments
@@ -116,31 +109,17 @@ curl -X DELETE https://content.luanti.org/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 [Luanti Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
* Converts the long description to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* 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 keys:
* Returns JSON dictionary with following key:
* `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
@@ -184,20 +163,20 @@ curl -X DELETE https://content.luanti.org/api/delete-token/ \
You can download a package by building one of the two URLs:
```
https://content.luanti.org/packages/${author}/${name}/download/`
https://content.luanti.org/packages/${author}/${name}/releases/${release}/download/`
https://content.minetest.net/packages/${author}/${name}/download/`
https://content.minetest.net/packages/${author}/${name}/releases/${release}/download/`
```
Examples:
```bash
# Edit package
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
curl -X PUT https://content.minetest.net/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.luanti.org/api/packages/username/name/ \
curl -X PUT https://content.minetest.net/api/packages/username/name/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "website": null }'
```
@@ -219,8 +198,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 Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
* `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`.
Sorting query parameters:
@@ -233,7 +212,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 Luanti client.
* `short`: stuff needed for the Minetest client.
* `vcs`: `short` but with `repo`.
@@ -253,8 +232,8 @@ Format query parameters:
* `url`: download URL
* `commit`: commit hash or null
* `downloads`: number of downloads
* `min_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `max_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `min_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* `max_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* `size`: size of zip file, in bytes.
* `package`
* `author`: author username
@@ -263,8 +242,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 Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
* `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`.
* 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)
@@ -279,7 +258,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 Luanti Versions [using the content's .conf file](/help/package_config/).
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
* Requires authentication.
* Deletes release.
@@ -288,7 +267,7 @@ Examples:
```bash
# Create release from Git
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{
"method": "git",
@@ -299,17 +278,17 @@ curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/
}'
# Create release from zip upload
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
curl -X POST https://content.minetest.net/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.luanti.org/api/packages/username/name/releases/new/ \
curl -X POST https://content.minetest.net/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.luanti.org/api/packages/username/name/releases/3/ \
curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/3/ \
-H "Authorization: Bearer YOURTOKEN"
```
@@ -350,26 +329,26 @@ Examples:
```bash
# Create screenshot
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
curl -X POST https://content.minetest.net/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.luanti.org/api/packages/username/name/screenshots/new/ \
curl -X POST https://content.minetest.net/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.luanti.org/api/packages/username/name/screenshots/3/ \
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
-H "Authorization: Bearer YOURTOKEN"
# Reorder screenshots
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/order/ \
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]"
# Set cover image
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/cover-image/ \
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/cover-image/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{ 'cover_image': 123 }"
```
@@ -479,7 +458,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:
@@ -489,7 +468,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.
@@ -519,7 +498,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
@@ -527,14 +506,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
### Luanti Versions
### Minetest 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.
@@ -542,7 +521,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.
@@ -573,11 +552,13 @@ 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 [Luanti Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
* Converts HTML or Markdown to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* Post data: HTML or Markdown as plain text.
* Content-Type: `text/html` or `text/markdown`.

View File

@@ -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 Luanti. Doesn't give much information other than "food"
description = The food mod for Luanti
# Bad, we know this is a mod for Minetest. Doesn't give much information other than "food"
description = The food mod for Minetest
# 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 Luanti, see
For a preview of what your package will look like inside Minetest, 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.luanti.org/packages/Warr1024/nodecore/) is a good
[NodeCore](https://content.minetest.net/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,18 +55,18 @@ 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 Luanti,
* Note: you should avoid images in the long description as they won't be visible inside Minetest,
when support for showing the long description is added.
## Localize / Translate your package
According to Google Play, 64% of Luanti Android users don't have English as their main language.
According to Google Play, 64% of Minetest 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 - Luanti Modding Book") }}
{{ _("Translation - Minetest Modding Book") }}
</a>
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
{{ _("Translating content meta - lua_api.md") }}

View File

@@ -6,7 +6,7 @@ your client to use new flags.
## Flags
Luanti allows you to specify a comma-separated list of flags to hide in the
Minetest 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.

View File

@@ -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.luanti.org/packages/Calinou/gauges/) by Calinou, CC0.
* health_*.png from [Gauges](https://content.minetest.net/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 Luanti mods/games
* Other Minetest 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).

View File

@@ -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 Luanti?
### How do I learn how to make mods and games for Minetest?
You should read
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest.
### How do I install something from here?

View File

@@ -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 Luanti client.
featured packages may be shown inside the Minetest client.
The purpose is to promote content that demonstrates a high quality of what is
possible in Luanti. The selection should be varied, and should vary over time.
possible in Minetest. 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 Luanti release.
* MUST: Be compatible with the latest stable Minetest 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 Luanti) wouldn't get completely
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Minetest) 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

View File

@@ -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.luanti.org/packages/AUTHOR/NAME/releases_feed.atom
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.json
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.atom
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.json
```

View File

@@ -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 Luanti.
suffixes are ignored, just like in Minetest.
supported_games = minetest_game, repixture
unsupported_games = lordofthetest, nodecore, whynot

View File

@@ -1,5 +1,5 @@
title: How to install mods, games, and texture packs
description: A guide to installing mods, games, and texture packs in Luanti.
description: A guide to installing mods, games, and texture packs in Minetest.
## Installing from the main menu (recommended)
@@ -7,8 +7,8 @@ description: A guide to installing mods, games, and texture packs in Luanti.
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 Luanti 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 Minetest 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 Luanti.
<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 Luanti">
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in minetest">
</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 Luanti.
<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 Luanti">
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in minetest">
</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 Luanti.
Troubleshooting:
* I can't find it in the ContentDB dialog (Browse online content)
* Make sure that you're on the latest version of Luanti.
* Make sure that you're on the latest version of Minetest.
* 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 Luanti">
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Minetest">
</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: wherever you extracted or installed Luanti to.
* Windows: whereever you extracted or installed Minetest to.
* Linux: usually `~/.minetest/`
4. Open or create the folder for the type of content (`mods`, `games`, or `textures`)
5. Git clone there

View File

@@ -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.**
Luanti is free and open source software, and is only as big as it is now
Minetest 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 Luanti avoid ending up in
Limiting the promotion of problematic licenses helps Minetest avoid ending up in
such a state. Licenses that prohibit redistribution or modification are
completely banned from ContentDB and the Luanti forums. Other non-free licenses
completely banned from ContentDB and the Minetest 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 Luanti content:
Here's a quick summary related to Minetest 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.

View File

@@ -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.luanti.org/oauth/authorize/
https://content.minetest.net/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.luanti.org/oauth/token/ \
curl -X POST https://content.minetest.net/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.luanti.org/api/whoami/ \
curl https://content.minetest.net/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN"
```

View File

@@ -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 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).
* `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).
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 Luanti Versions
### Min and Max Minetest Versions
<a name="min_max_versions" />
When creating a release, the `.conf` file will be read to determine what Luanti
When creating a release, the `.conf` file will be read to determine what Minetest
versions the release supports. If the `.conf` doesn't specify, then it is assumed
that it supports all versions.

View File

@@ -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.luanti.org/github/webhook/`
4. Set the payload URL to `https://content.minetest.net/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.luanti.org/gitlab/webhook/`
4. Set the URL to `https://content.minetest.net/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 Luanti versions, which files are included,
From the Git repository, you can set the min/max Minetest versions, which files are included,
and update the package meta.

View File

@@ -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 Luanti versions, which files are included,
From the Git repository, you can set the min/max Minetest versions, which files are included,
and update the package meta.

View File

@@ -1,6 +1,25 @@
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>
@@ -18,4 +37,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/meeting-minutes/minutes20090304)
3. [OSI](https://opensource.org/minutes20090304)

View File

@@ -1,5 +1,22 @@
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
@@ -9,53 +26,33 @@ including ones not covered by this document, and to ban users who abuse this ser
## 2. Accepted Content
### 2.1. Mature Content
### 2.1. Acceptable Content
See the [Terms of Service](/terms/) for a full list of prohibited content.
Sexually-orientated content is not permitted.
If in doubt at what this means, [contact us by raising a report](/report/).
Other mature content is permitted providing that it is labelled with the applicable
[content warning](/help/content_flags/).
Mature content is permitted providing that it is labelled correctly.
See [Content Flags](/help/content_flags/).
### 2.2. Useful Content / State of Completion
### 2.2. State of Completion
ContentDB is for playable and useful content - content which is sufficiently
complete to be useful to end-users.
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.
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.
You should make sure to mark Work in Progress stuff as such in the "maintenance
status" column, 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 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.
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.
## 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.
@@ -73,46 +70,23 @@ 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. Forks and Reimplementations
### 3.2. Mod 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
must be possible to use the new mod as a drop-in replacement.
should 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. License file
### 4.1. Allowed Licenses
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.
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
that you have used in your package. For help on doing copyright correctly, see
the [Copyright help page](/help/copyright/).
**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.
@@ -122,18 +96,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 reject custom/untested licenses and reserve the right
to decide whether a license should be included.
get around to adding it. We tend to 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.3. Recommended Licenses
### 4.2. 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 Luanti by default. See the help page
result in your package not being shown in Minetest 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
@@ -176,14 +150,10 @@ Doing so may result in temporary or permanent suspension from ContentDB.
## 7. Screenshots
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.
1. **Screenshots must not violate copyright.** You should have the rights to the
screenshot.
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
2. **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
@@ -199,9 +169,20 @@ 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.
4. **Screenshots must only contain content appropriate for the Content Warnings of
3. **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

View File

@@ -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 Luanti staff members (moderators + core devs).
* Encrypted backups may be shared with selected Minetest 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.

15
app/flatpages/rules.md Normal file
View File

@@ -0,0 +1,15 @@
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.

View File

@@ -1,133 +0,0 @@
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.

View File

@@ -13,7 +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/>.
import sys
from typing import List, Dict, Optional, Tuple
import sqlalchemy
@@ -90,6 +90,7 @@ class GSPackage:
return self.user_unsupported_games
def add_error(self, error: str):
print(f"ERROR {self.name}: {error}")
return self.errors.add(error)
@@ -135,10 +136,12 @@ class GameSupport:
return [package for package in self.packages.values() if modname in package.depends]
def _get_supported_games_for_modname(self, depend: str, visited: list[str]):
print(f"_get_supported_games_for_modname {depend} visited {', '.join(visited)}", file=sys.stderr)
dep_supports_all = False
for_dep = set()
for provider in self.get_all_that_provide(depend):
found_in = self._get_supported_games(provider, visited)
print(f" - provider for {depend}: {provider.name}: {found_in}", file=sys.stderr)
if found_in is None:
# Unsupported, keep going
pass
@@ -151,6 +154,7 @@ class GameSupport:
return dep_supports_all, for_dep
def _get_supported_games_for_deps(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
print(f"_get_supported_games_for_deps package {package.name} visited {', '.join(visited)}", file=sys.stderr)
ret = set()
for depend in package.depends:
@@ -173,18 +177,21 @@ class GameSupport:
return ret
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
print(f"_get_supported_games package {package.name} visited {', '.join(visited)}", file=sys.stderr)
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)
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:
print(f"_get_supported_games package {package.name} is game", file=sys.stderr)
return {package.name}
elif package.is_confirmed:
print(f"_get_supported_games package {package.name} is confirmed", file=sys.stderr)
return package.supported_games
visited = visited.copy()
@@ -221,6 +228,7 @@ class GameSupport:
while len(to_update) > 0:
current_package = to_update.pop()
print(f"on_update package {current_package.name}", file=sys.stderr)
if current_package.id_ in self.packages and current_package.type != PackageType.GAME:
self._get_supported_games(current_package, [])

View File

@@ -107,7 +107,7 @@ def validate(data: dict):
def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, data: dict,
reason: str = None) -> bool:
reason: str = None):
if not package.check_perm(user, Permission.EDIT_PACKAGE):
raise LogicError(403, lazy_gettext("You don't have permission to edit this package"))
@@ -192,11 +192,9 @@ 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)
was_modified = was_new
if not was_new:
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)
@@ -210,7 +208,6 @@ 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))
if was_modified:
db.session.commit()
db.session.commit()
return was_modified
return package

View File

@@ -28,7 +28,7 @@ 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, name: str):
def check_can_create_release(user: User, package: Package):
if not package.check_perm(user, Permission.MAKE_RELEASE):
raise LogicError(403, lazy_gettext("You don't have permission to make releases"))
@@ -37,13 +37,10 @@ def check_can_create_release(user: User, package: Package, name: str):
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, name)
check_can_create_release(user, package)
rel = PackageRelease()
rel.package = package
@@ -72,7 +69,7 @@ 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,
commit_hash: str = None):
check_can_create_release(user, package, name)
check_can_create_release(user, package)
if commit_hash:
commit_hash = commit_hash.lower()

214
app/markdown.py Normal file
View File

@@ -0,0 +1,214 @@
# 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])

View File

@@ -1,113 +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 typing import Sequence
from urllib.parse import urljoin
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, url: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[href]")
return set([urljoin(url, x.get("href")) for x in links])

View File

@@ -1,97 +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 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)

View File

@@ -1,109 +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/>.
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)

View File

@@ -158,7 +158,7 @@ class ForumTopic(db.Model):
@property
def url(self):
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.topic_id)
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.topic_id)
def get_repo_url(self):
if self.link is None:

View File

@@ -457,7 +457,7 @@ class Package(db.Model):
if self.forums is None:
return None
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.forums)
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.forums)
enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True)
@@ -679,7 +679,6 @@ 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,
@@ -812,7 +811,7 @@ class Package(db.Model):
elif perm == Permission.APPROVE_SCREENSHOT:
return (is_maintainer or is_approver) and \
user.rank.at_least(UserRank.TRUSTED_MEMBER if self.approved else UserRank.NEW_MEMBER)
user.rank.at_least(UserRank.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)
@@ -1071,7 +1070,8 @@ class MinetestRelease(db.Model):
if version:
parts = version.strip().split(".")
if len(parts) >= 2:
query = MinetestRelease.query.filter(func.replace(MinetestRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
major_minor = parts[0] + "." + parts[1]
query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
if protocol_num:
query = query.filter_by(protocol=protocol_num)

View File

@@ -260,7 +260,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.luanti.org")
return get_gravatar(self.email or f"{self.username}@content.minetest.net")
def check_perm(self, user, perm):
if not user.is_authenticated:

View File

@@ -171,7 +171,7 @@ async function load_data() {
const data = {
datasets: [
{ label: "Web / other", data: getData(json.platform_other) },
{ label: "Luanti", data: getData(json.platform_minetest) },
{ label: "Minetest", data: getData(json.platform_minetest) },
],
};
setup_chart(ctx, data, annotations);

View File

@@ -26,7 +26,7 @@ window.addEventListener("load", () => {
try {
const pasteData = e.clipboardData.getData('text');
const url = new URL(pasteData);
if (url.hostname === "forum.luanti.org") {
if (url.hostname === "forum.minetest.net") {
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.luanti.org/viewtopic.php?t=" + forumsField.value, "_blank");
window.open("https://forum.minetest.net/viewtopic.php?t=" + forumsField.value, "_blank");
});
function setupHints(id, hints) {

View File

@@ -22,7 +22,7 @@ function sleep(interval) {
}
async function pollTask(poll_url, disableTimeout, onProgress) {
async function pollTask(poll_url, disableTimeout) {
let tries = 0;
while (true) {
@@ -42,10 +42,6 @@ async function pollTask(poll_url, disableTimeout, onProgress) {
console.error(e);
}
if (res && res.status) {
onProgress?.(res);
}
if (res && res.status === "SUCCESS") {
console.log("Got result")
return res.result;
@@ -66,38 +62,3 @@ 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() { location.reload() })
}
});

View File

@@ -3,7 +3,7 @@
<ShortName>ContentDB</ShortName>
<LongName>ContentDB</LongName>
<InputEncoding>UTF-8</InputEncoding>
<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}"/>
<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}"/>
</OpenSearchDescription>

View File

@@ -51,19 +51,6 @@ h3 {
letter-spacing: .05em
}
h1, h2, h3, h4, h5, h6 {
&::after {
display: block;
content: "";
clear: both;
}
}
.header-anchor {
float: right;
opacity: 0.8;
}
.badge-notify {
background:yellow; /* #00bc8c;*/
color: black;

View File

@@ -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'], task_track_started=True)
broker=app.config['CELERY_BROKER_URL'])
celery.init_app(app)
return celery

View File

@@ -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.luanti.org", forums_username)
profile = get_profile("https://forum.minetest.net", 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.luanti.org/", pic)
pic = urljoin("https://forum.minetest.net/", 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.luanti.org") or force_replace_pic
if pic_needs_replacing and pic.startswith("https://forum.luanti.org"):
user.profile_pic.startswith("https://forum.minetest.net") or force_replace_pic
if pic_needs_replacing and pic.startswith("https://forum.minetest.net"):
print(f"####### Queueing", file=sys.stderr)
set_profile_picture_from_url.delay(user.username, pic)

View File

@@ -20,7 +20,7 @@ import os
import shutil
import sys
from json import JSONDecodeError
from zipfile import ZipFile, BadZipFile
from zipfile import ZipFile
import gitdb
from flask import url_for
@@ -268,8 +268,11 @@ def update_translations(package: Package, tree: PackageTreeNode):
)
conn.execute(stmt)
raw_translations = tree.get_translations(tree.get("textdomain", tree.name), allowed_languages=allowed_languages)
raw_translations = tree.get_translations(tree.get("textdomain", tree.name))
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)),
@@ -303,16 +306,13 @@ def _check_zip_file(temp_dir: str, zf: ZipFile) -> bool:
def _safe_extract_zip(temp_dir: str, archive_path: str) -> bool:
try:
with ZipFile(archive_path, 'r') as zf:
if not _check_zip_file(temp_dir, zf):
return False
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)
except BadZipFile as e:
raise TaskError(str(e))
# Extract all
for member in zf.infolist():
zf.extract(member, temp_dir)
return True
@@ -342,15 +342,16 @@ def check_zip_release(self, id, path):
def check_all_zip_files():
result = []
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,
})
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,
})
return json.dumps(result)

View File

@@ -25,7 +25,7 @@ from .config import parse_conf
from .translation import Translation, parse_tr
basenamePattern = re.compile("^([a-z0-9_]+)$")
licensePattern = re.compile("^licen[sc]e[^/.]*(\.(txt|md))?$", re.IGNORECASE)
licensePattern = re.compile("^(licen[sc]e|copying)(.[^/\n]+)?$", re.IGNORECASE)
DISALLOWED_NAMES = {
"core", "minetest", "group", "table", "string", "lua", "luajit", "assert", "debug",
@@ -235,12 +235,9 @@ class PackageTreeNode:
# Calculate short description
if "description" in result:
desc = result["description"]
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
idx = desc.find(".") + 1
cutIdx = min(len(desc), 200 if idx < 5 else idx)
result["short_description"] = desc[:cutIdx]
if "name" in result:
self.name = result["name"]
@@ -316,15 +313,10 @@ class PackageTreeNode:
return ret
def get_translations(self, textdomain: str, allowed_languages: set[str]) -> list[Translation]:
def get_translations(self, textdomain: 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:

View File

@@ -24,7 +24,6 @@ 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
@@ -45,7 +44,7 @@ def update_package_scores():
def desc_contains(desc: str, search_str: str):
if search_str.startswith("https://forum.luanti.org/viewtopic.php?%t="):
if search_str.startswith("https://forum.minetest.net/viewtopic.php?%t="):
reg = re.compile(search_str.replace(".", "\\.").replace("/", "\\/").replace("?", "\\?").replace("%", ".*"))
return reg.search(desc)
else:
@@ -58,7 +57,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.luanti.org/viewtopic.php?%t={pair[1]}"))
package_links.append((pair[0], f"https://forum.minetest.net/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()
@@ -111,15 +110,12 @@ 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.luanti.org/)",
"User-Agent": "Mozilla/5.0 (compatible; ContentDB link checker; +https://content.minetest.net/)",
}
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:
@@ -129,8 +125,6 @@ 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", ""))
links: set[Optional[str]] = {
package.repo,
package.website,
@@ -156,9 +150,6 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
if url.scheme != "http" and url.scheme != "https":
continue
if url.hostname in ignored_urls:
continue
res = _url_exists(link)
if res != "":
bad_urls[link] = res
@@ -189,7 +180,7 @@ def check_package_on_submit(package_id: int):
msg = _check_package(package)
if msg:
marked = f"Marked {package.title} as {PackageState.CHANGES_NEEDED.value}"
marked = f"Marked {package.title} as Changed Needed"
system_user = get_system_user()
post_to_approval_thread(package, system_user, marked, is_status_update=True, create_thread=True)

View File

@@ -25,13 +25,10 @@ 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_urls = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
if discord_urls is None:
discord_url = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
if discord_url is None:
return
if isinstance(discord_urls, str):
discord_urls = [discord_urls]
json = {
"content": content[0:2000],
}
@@ -55,8 +52,7 @@ def post_discord_webhook(username: Optional[str], content: str, is_queue: bool,
json["embeds"] = [embed]
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()
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()

View File

@@ -15,70 +15,45 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import subprocess
import sys
from subprocess import Popen, PIPE, TimeoutExpired
from typing import Optional, List
from subprocess import Popen, PIPE
from typing import Optional
from app.models import Package, PackageState, PackageRelease
from app.tasks import celery
@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())
@celery.task()
def search_in_releases(query: str, file_filter: str):
packages = list(Package.query.filter(Package.state == PackageState.APPROVED).all())
running = []
results = []
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=15)
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
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]
exit_code = handle.poll()
if exit_code is None:
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
handle.kill()
results.append({
"package": package.as_key_dict(),
"lines": "Error: timeout",
})
continue
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(),
})
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}",
})
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()
return {
"query": query,

View File

@@ -23,7 +23,7 @@ from flask_login import current_user
from markupsafe import Markup
from . import app, utils
from app.markdown import get_headings
from .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

View File

@@ -4,7 +4,7 @@
{% if version %}
Edit {{ version.name }}
{% else %}
New Luanti Version
New Minetest Version
{% endif %}
{% endblock %}

View File

@@ -1,13 +1,13 @@
{% extends "base.html" %}
{% block title %}
{{ _("Luanti Versions") }}
{{ _("Minetest Versions") }}
{% endblock %}
{% block content %}
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_version') }}">{{ _("New Version") }}</a>
<h1>{{ _("Luanti Versions") }}</h1>
<h1>{{ _("Minetest Versions") }}</h1>
<div class="list-group">
{% for v in versions %}

View File

@@ -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=55">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=54">
<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 &copy; 2018-24 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
ContentDB &copy; 2018-23 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='terms') }}">{{ _("Terms of Service") }}</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='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>
@@ -285,11 +285,9 @@
<input type="submit" class="btn btn-sm btn-secondary" value="{{ _('Hide non-free packages') }}">
{% endif %}
</form>
{% if false %}
<p class="text-warning">
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
</p>
{% endif %}
<p class="text-warning">
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
</p>
{% if debug %}
<p style="color: red">

View File

@@ -5,7 +5,7 @@
{% endblock %}
{% block description %}
{{ _("Welcome to the best place to find Luanti mods, games, and texture packs") }}
{{ _("Welcome to the best place to find Minetest mods, games, and texture packs") }}
{% endblock %}
{% block scriptextra %}
@@ -13,10 +13,10 @@
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://content.luanti.org/",
"url": "https://content.minetest.net/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://content.luanti.org/packages?q={search_term_string}",
"target": "https://content.minetest.net/packages?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
@@ -39,25 +39,24 @@
</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=meta.title, author=package.author.display_name) }}">
alt="{{ _('%(title)s by %(author)s', title=package.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=meta.title, author=package.author.display_name) }}
{{ _('<strong>%(title)s</strong> by %(author)s', title=package.title, author=package.author.display_name) }}
{% else %}
<strong>{{ meta.title }}</strong>
<strong>{{ package.title }}</strong>
{% endif %}
</h3>
<p>
{{ meta.short_desc }}
{{ package.short_desc }}
</p>
{% if package.author %}
<div class="d-none d-md-block">

View File

@@ -103,10 +103,10 @@
<h3 class="mt-5">{{ _("Downloads by Reason") }}</h3>
<ul>
<li>{{ _("<b>New Install</b>: the user clicked [Install] inside of Luanti.") }}</li>
<li>{{ _("<b>New Install</b>: the user clicked [Install] inside of Minetest.") }}</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 Luanti version (before 5.5).") }}</li>
<li>{{ _("<b>Other / Unknown</b>: downloaded by a web browser or an outdated Minetest version (before 5.5).") }}</li>
</ul>
<p class="text-muted">
{{ _("This is a stacked area graph. For total downloads, look at the combined height.") }}

View File

@@ -14,7 +14,7 @@
[{{ topic.type.text }}]
</td>
<td>
<a href="https://forum.luanti.org/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
<a href="https://forum.minetest.net/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.luanti.org/viewtopic.php?t={{ topic.topic_id}}">
<a class="list-group-item list-group-item-action" href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">
<span class="float-end text-muted">
{{ topic.created_at | date }}
</span>

View File

@@ -23,7 +23,7 @@
{% for t in similar_topics %}
<li>
[{{ t.type.text }}]
<a href="https://forum.luanti.org/viewtopic.php?t={{ t.topic_id }}">
<a href="https://forum.minetest.net/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 %}

View File

@@ -64,8 +64,8 @@
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ 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.title) }}
{{ render_field(form.description, hint=_("Shown to users when you request access to their account")) }}
{{ render_field(form.redirect_url) }}
{{ render_field(form.app_type, hint=_("Where will you store your client_secret?")) }}

View File

@@ -18,7 +18,7 @@
{{ form_scripts() }}
{{ easymde_scripts() }}
{% if enable_wizard %}
<script src="/static/js/polltask.js?v=3"></script>
<script src="/static/js/polltask.js"></script>
<script src="/static/js/package_create.js"></script>
{% endif %}
<script src="/static/js/package_edit.js?v=3"></script>
@@ -137,7 +137,7 @@
{{ render_field(form.issueTracker, class_="pkg_meta", hint=_("Where should users report issues?")) }}
{{ render_field_prefix_button(form.forums, class_="pkg_meta",
pattern="[0-9]+",
prefix="forum.luanti.org/viewtopic.php?t=",
prefix="forum.minetest.net/viewtopic.php?t=",
placeholder=_("Paste a forum topic URL"),
has_view=True) }}
{{ render_field(form.video_url, class_="pkg_meta", hint=_("YouTube videos will be shown in an embed.")) }}

View File

@@ -51,7 +51,7 @@
{% endif %}
{% endif %}
<h3 class="mt-5">{{ _("Supported Luanti versions") }}</h3>
<h3 class="mt-5">{{ _("Supported Minetest versions") }}</h3>
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
@@ -67,12 +67,12 @@
<strong>
{{ _("Are you sure your package doesn't work on versions after %(version)s?", version=last.label) }}
</strong>
{{ _("Only set the maximum version if you know that it doesn't work on newer Luanti versions.") }}
{{ _("Only set the maximum version if you know that it doesn't work on newer Minetest versions.") }}
{{ _("Don't set the maximum version just because you haven't tested it on newer versions.") }}
<p>
<p>
{{ _("Set the minimum and maximum Luanti versions supported.
{{ _("Set the minimum and maximum Minetest versions supported.
This release will be hidden to clients outside of that range. ") }}
<br />
{{ _("Leave both as None if in doubt.") }}

View File

@@ -59,7 +59,7 @@
tips on customising releases.") }}
</p>
<h3 class="mt-5">{{ _("3. Supported Luanti versions") }}</h3>
<h3 class="mt-5">{{ _("3. Supported Minetest versions") }}</h3>
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
@@ -75,7 +75,7 @@
<strong>
{{ _("Are you sure your package doesn't work on versions after %(version)s?", version=last.label) }}
</strong>
{{ _("Only set the maximum version if you know that it doesn't work on newer Luanti versions.") }}
{{ _("Only set the maximum version if you know that it doesn't work on newer Minetest versions.") }}
{{ _("Don't set the maximum version just because you haven't tested it on newer versions.") }}
<p>
@@ -86,7 +86,7 @@
</p>
<p>
{{ _("Set the minimum and maximum Luanti versions supported.
{{ _("Set the minimum and maximum Minetest versions supported.
This release will be hidden to clients outside of that range. ") }}
<br>
{{ _("Leave both as None if in doubt.") }}

View File

@@ -12,7 +12,7 @@
<p>
{{ _("A release is a single downloadable version of your %(title)s.", title=package.type.text.lower()) }}
{{ _("You need to create releases even if you use a rolling release development cycle, as Luanti needs them to check for updates.") }}
{{ _("You need to create releases even if you use a rolling release development cycle, as Minetest needs them to check for updates.") }}
</p>
{% if package.repo %}

View File

@@ -7,7 +7,6 @@
{% block content %}
<h2 class="mt-0">{{ self.title() }}</h2>
{% if package.approved %}
<form method="POST" action="">
<h3>{{ _("Change maintenance state") }}</h3>
<p>
@@ -30,7 +29,6 @@
{% endfor %}
</p>
</form>
{% endif %}
<form method="POST" action="" class="mt-5">
<h3>{{ _("Remove") }}</h3>
@@ -46,11 +44,6 @@
{{ _("Unapproving a package will put it back into Draft, where
it can be submitted for approval again.") }}
</p>
{% else %}
<p>
<strong>{{ _("You don't need to delete a package just to change something.") }}</strong>
{{ _("Click 'Edit' at the top right of the package page.") }}
</p>
{% endif %}
{% if hard_deps %}

View File

@@ -17,7 +17,7 @@
<h1>{{ _("Post a review for %(title)s by %(author)s", title=self.link(), author=package.author.display_name) }}</h1>
<p class="alert alert-primary">
{{ _("Please make sure you read ContentDB's <a href='/terms/'>Terms of Service</a>") }}
{{ _("Please make sure you read ContentDB's <a href='/rules/'>rules</a>") }}
</p>
{% if package.issueTracker %}

View File

@@ -34,7 +34,7 @@
{% for t in similar_topics %}
<li>
[{{ t.type.value }}]
<a href="https://forum.luanti.org/viewtopic.php?t={{ t.topic_id }}">
<a href="https://forum.minetest.net/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 %}

View File

@@ -20,12 +20,24 @@
</p>
<p>
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
{{ _("Translation - Luanti Modding Book") }}
{{ _("Translation - Minetest Modding Book") }}
</a>
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
{{ _("Translating content meta - lua_api.md") }}
</a>
</p>
<h3 id="template">{{ _("Translation template") }}</h3>
<p>
{{ _("To quickly add support for ContentDB package translation, create a file at %(location)s with the following content:",
location="<code>locale/template.txt</code>"|safe) }}
</p>
<pre><code># textdomain: {{ package.name }}
{{ package.title | replace("@", "@@") | replace("=", "@=") }} =
{{ package.short_desc | replace("@", "@@") | replace("=", "@=") }} =
</code></pre>
{% else %}
<p>
{{ _("%(title)s is available in %(num)d languages.", title=package.title, num=num) }}
@@ -77,34 +89,5 @@
</div>
{% endfor %}
{% endif %}
{% if not has_content_translations %}
{% set translation_template_path %}
{% if package.type == package.type.GAME %}
<code>mods/mymod/locale/template.txt</code>
{% else %}
<code>locale/template.txt</code>
{% endif %}
{% endset %}
<h3 id="template">{{ _("Translation template") }}</h3>
<p>
{{ _("To quickly add support for ContentDB package translation, create a file at %(location)s with the following content:",
location=translation_template_path) }}
</p>
<pre><code># textdomain: {{ package.name }}
{{ package.title | replace("@", "@@") | replace("=", "@=") }} =
{{ package.short_desc | replace("@", "@@") | replace("=", "@=") }} =
</code></pre>
{% if package.type == package.type.GAME %}
<p>{{ _("With games, you also need to name the textdomain in game.conf:") }}</p>
<pre><code>textdomain = mymod</code></pre>
<p>{{ _("Replace mymod with the name of mod / textdomain you chose.") }}</p>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@@ -53,11 +53,11 @@
{% if release and (release.min_rel or release.max_rel) %}
<small class="count display-block">
{% if release.min_rel and release.max_rel %}
{{ _("Luanti %(min)s - %(max)s", min=release.min_rel.name, max=release.max_rel.name) }}
{{ _("Minetest %(min)s - %(max)s", min=release.min_rel.name, max=release.max_rel.name) }}
{% elif release.min_rel %}
{{ _("For Luanti %(min)s and above", min=release.min_rel.name) }}
{{ _("For Minetest %(min)s and above", min=release.min_rel.name) }}
{% elif release.max_rel %}
{{ _("Luanti %(max)s and below", max=release.max_rel.name) }}
{{ _("Minetest %(max)s and below", max=release.max_rel.name) }}
{% endif %}
</small>
{% endif %}

View File

@@ -10,17 +10,18 @@
{% block content %}
<h1>{{ self.title() }}</h1>
<article data-task-id="{{ info.id }}">
<p id="status"></p>
<div id="progress" class="progress d-none">
<div class="progress-bar bg-info" role="progressbar" style="width: 50%;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</article>
{% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %}
<pre style="white-space: pre-wrap; word-wrap: break-word;">{{ info.error }}</pre>
{% else %}
<script src="/static/js/polltask.js?v=3"></script>
<script src="/static/js/polltask.js"></script>
<script>
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
pollTask("{{ url_for('tasks.check', id=info.id) }}", true)
.then(function() { location.reload() })
.catch(function() { location.reload() })
</script>
<noscript>
{{ _("Reload the page to check for updates.") }}
</noscript>

View File

@@ -40,6 +40,19 @@
</div>
</div>
{% if allow_private_change %}
{{ render_checkbox_field(form.private, class_="my-3") }}
{% elif form.private.data %}
<p>
Private.
</p>
{% endif %}
{% if allow_private_change or form.private.data %}
<p>
{{ _("Only you, the package author, and users of Approver rank and above can read private threads.") }}
</p>
{% endif %}
{{ render_submit_field(form.btn_submit) }}
</form>

View File

@@ -5,7 +5,7 @@
{%- endblock %}
{% block description -%}
{{ _("Help make Luanti more accessible by translating packages into other languages.") }}
{{ _("Help make Minetest more accessible by translating packages into other languages.") }}
{% endblock %}
{% macro render_packages(packages) %}

View File

@@ -5,7 +5,7 @@
{% endblock %}
{% block ruben_link %}
<a href="https://forum.luanti.org/ucp.php?i=pm&mode=compose&u=2051">rubenwardy</a>
<a href="https://forum.minetest.net/ucp.php?i=pm&mode=compose&u=2051">rubenwardy</a>
{% endblock %}
{% block pane %}
@@ -36,7 +36,7 @@
<td>Forums</td>
<td>
{% if user.forums_username %}
<a href="https://forum.luanti.org/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
{{ user.forums_username }}
</a>
{% else %}
@@ -59,11 +59,12 @@
{{ _("View ContentDB's GitHub Permissions") }}
</a>
{% endif %}
<form method="post" action="{{ url_for('users.disconnect_github', username=user.username) }}" class="d-inline-block">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-secondary" value="{{ _('Disconnect') }}" />
</form>
{% if user.forums_username %}
<form method="post" action="{{ url_for('users.disconnect_github', username=user.username) }}" class="d-inline-block">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-secondary" value="{{ _('Disconnect') }}" />
</form>
{% endif %}
{% elif user == current_user %}
<a class="btn btn-secondary" href="{{ url_for('vcs.github_start') }}">
{{ _("Link Github") }}
@@ -84,13 +85,11 @@
{% else %}
<p>
{{ _("Account Deletion and Deactivation isn't available to users yet.") }}
{{ _("Please raise a report to request account deletion.") }}
</p>
<p>
<a class="btn btn-secondary" href="{{ url_for('report.report', url=url_current(), message="Delete my account") }}">{{ _("Report") }}</a>
{{ _("Please contact the admin.") }}
</p>
{% endif %}
<h3 class="mt-5">{{ _("Recent Account Actions") }}</h3>
{% from "macros/audit_log.html" import render_audit_log %}

View File

@@ -15,9 +15,6 @@
{{ render_field(form.email, tabindex=220,
hint=_("Your email is needed to recover your account if you forget your password and to send (configurable) notifications. ") +
_("Your email will never be shared with a third-party.")) }}
<p>
{{ _("Note: protonmail is unsupported by ContentDB. <a href='https://forum.luanti.org/viewtopic.php?t=30709'>More info</a>.") }}
</p>
{% endif %}
{% if form.old_password %}

View File

@@ -7,7 +7,7 @@
{% block content %}
<h1>{{ self.title() }}</h1>
<h2>{{ _("Do you have an account on the Luanti Forums?") }}</h2>
<h2>{{ _("Do you have an account on the Minetest Forums?") }}</h2>
<p>
{{ _("ContentDB will link your account to your forum account if you have one, but you don't need one.") }}
@@ -20,7 +20,7 @@
<a class="btn btn-primary me-3" href="{{ url_for('users.register') }}">
{{ _("<b>No</b>, I don't have one") }}
</a>
<a class="btn btn-secondary" href="https://forum.luanti.org/ucp.php?mode=register">
<a class="btn btn-secondary" href="https://forum.minetest.net/ucp.php?mode=register">
{{ _("Create forum account") }}
</a>
</p>

View File

@@ -19,10 +19,6 @@ Create Account from Forums User
{{ _("You can still <a href='%(url)s'>sign up without one</a>.", url=url_for('users.register')) }}
</p>
<p>
{{ _("By signing up, you agree to the <a href='/terms/' target='_blank'>Terms of Service</a> and <a href='/privacy_policy/' target='_blank'>Privacy Policy</a>.") }}
</p>
<div class="row mt-5">
<div class="col-sm-6">
<div class="card">
@@ -45,7 +41,7 @@ Create Account from Forums User
<p>
{{ _("You'll need to have the GitHub field in your forum profile filled out.") }}
{{ _("Log into the forum and <a href='https://forum.luanti.org/ucp.php?i=173'>do that here</a>.") }}
{{ _("Log into the forum and <a href='https://forum.minetest.net/ucp.php?i=173'>do that here</a>.") }}
</p>
<input class="btn btn-primary" type="submit" value="{{ _('Next: log in with GitHub') }}">
@@ -72,7 +68,7 @@ Create Account from Forums User
placeholder="{{ _('Forum username') }}" pattern="[a-zA-Z0-9._ -]+" title="{{ _('Only a-zA-Z0-9._ allowed') }}" required>
<p>
{{ _("Go to <a href='https://forum.luanti.org/ucp.php?i=profile&mode=signature'>User Control Panel &gt; Profile &gt; Edit signature</a>") }}
{{ _("Go to <a href='https://forum.minetest.net/ucp.php?i=profile&mode=signature'>User Control Panel &gt; Profile &gt; Edit signature</a>") }}
</p>
<p>

View File

@@ -71,7 +71,7 @@
</span>
{% if user.forums_username %}
<a class="btn" href="https://forum.luanti.org/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
<a class="btn" href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
<i class="fas fa-comments"></i>
<span class="count">
{{ _("Forums") }}

View File

@@ -11,7 +11,7 @@
<div class="row">
<div class="col-md-2">
{% if user.forums_username %}
<a href="https://forum.luanti.org/ucp.php?i=profile&mode=avatar">
<a href="https://forum.minetest.net/ucp.php?i=profile&mode=avatar">
{% elif user.email %}
<a href="https://en.gravatar.com/">
{% endif %}

View File

@@ -23,9 +23,6 @@
{{ render_field(form.email,
hint=_("Your email is needed to recover your account if you forget your password and to send (configurable) notifications. ") +
_("Your email will never be shared with a third-party.")) }}
<p>
{{ _("Note: protonmail is unsupported by ContentDB. <a href='https://forum.luanti.org/viewtopic.php?t=30709'>More info</a>.") }}
</p>
{{ render_field(form.password, hint=_("Must be at least 12 characters long.")) }}
@@ -34,10 +31,15 @@
</p>
{{ render_field(form.question, hint=_("Please prove that you are human")) }}
<p>
{{ _("By signing up, you agree to the <a href='/terms/' target='_blank'>Terms of Service</a> and <a href='/privacy_policy/' target='_blank'>Privacy Policy</a>.") }}
</p>
{% set label %}
{{ _("I agree to the ") }}
<a href="{{ url_for('flatpage', path='privacy_policy') }} ">
{{ _("Privacy Policy") }}
</a>
{% endset %}
{{ render_checkbox_field(form.agree, label=label, class_="my-4") }}
{# Submit button #}
<p>
{{ render_submit_field(form.submit, tabindex=180) }}
</p>

View File

@@ -23,9 +23,6 @@
{{ _("Your email is needed to recover your account if you forget your password, and to send (configurable) notifications.") }}
{{ _("Your email will never be shared with a third-party.") }}
</p>
<p>
{{ _("Note: protonmail is unsupported by ContentDB. <a href='https://forum.luanti.org/viewtopic.php?t=30709'>More info</a>.") }}
</p>
{% if user.email_verifications.filter_by(is_password_reset=False).count() > 0 %}
<p>

View File

@@ -17,7 +17,6 @@
{{ form.hidden_tag() }}
{{ render_field(form.query, hint=self.query_hint()) }}
{{ render_field(form.file_filter, hint="Supports wildcards and regex") }}
{{ render_field(form.type, hint=_("Use shift to select multiple. Leave selection empty to match any type.")) }}
{{ render_submit_field(form.submit, tabindex=180) }}
</form>

View File

@@ -217,6 +217,14 @@ def test_cycle_fails_safely():
"""
A dependency cycle shouldn't completely break the graph if a mod is
available elsewhere
a -> d
game has d
cycle:
d -> b
b -> c
c -> b
"""
support = GameSupport()
support.add(make_game("game1", ["default", "mod_d"]))
@@ -241,6 +249,68 @@ def test_cycle_fails_safely():
}
def test_cycle_not_fulfill_with_conflict():
"""
Test that cycles aren't fulfilled by installing a mod multiple times, which would conflict
a -> b -> a
game1 has a
b should be {game1}
a should be unfulfilled
"""
support = GameSupport()
support.add(make_game("game1", ["default", "mod_a"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["mod_a"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_first_run()
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
# Can't install mod_a and game1 at the same time
assert not modA.is_confirmed
assert modA.detected_supported_games == {}
assert support.all_errors == {
"author/mod_a: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
}
def test_cycle_not_fulfill_with_conflict2():
"""
Test that cycles aren't fulfilled by installing a mod multiple times, which would conflict
a -> b -> a
game1 has a
b should be {game1}
a should be unfulfilled
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["mod_a"]))
modA2 = support.add(make_mod("mod_a", ["mod_a"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_first_run()
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert modA2.is_confirmed
assert modA2.detected_supported_games == {"game1"}
# Can't install modA and modA2 at the same time
assert not modA.is_confirmed
assert modA.detected_supported_games == {}
assert support.all_errors == {
"author/mod_a: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
}
def test_update():
"""
Test updating a mod will update mods that depend on it

View File

@@ -56,7 +56,7 @@ def normalize_line_endings(value: Optional[str]) -> Optional[str]:
if value is None:
return None
return value.replace("\r\n", "\n")
return value.replace("\r\n", "\n").strip()
def should_return_json():

View File

@@ -322,28 +322,15 @@ def package_reviews_as_hypertext(package: Package, formspec_version: int = 7):
links[f"link_{link_counter}"] = url
return f"<action name=link_{link_counter}>{escape_hypertext(label)}</action>"
body += make_link(package.get_url("packages.review", absolute=True), gettext("Leave a review"))
body += "\n\n"
reviews = package.reviews.all()
for review in reviews:
for review in package.reviews:
review: PackageReview
html = render_markdown(review.thread.first_reply.comment)
content = html_to_minetest(html, package.get_url("packages.view", absolute=True),
formspec_version, False, f"review_{review.id}_")
links.update(content["links"])
comment_body = content["body"].rstrip()
formspec_version, False, f"review_{review.id}_")["body"].strip()
author = make_link(abs_url_for("users.profile", username=review.author.username), review.author.display_name)
rating = ["<thumbsdown>", "<thumbsdown>", "<neutral>", "<thumbsup>", "<thumbsup>"][review.rating - 1]
num_comments = review.thread.replies.count()
comments = make_link(abs_url_for("threads.view", id=review.thread.id), f"Comments [{num_comments}]")
positive, negative, _ = review.get_totals()
helpful = f"Review helpfulness: +{positive} / -{negative}"
body += f"{author} {rating}\n<big>{escape_hypertext(review.thread.title)}</big>\n{comment_body}\n{comments}{helpful}\n\n"
if len(reviews) == 0:
body += escape_hypertext(gettext("No reviews available."))
rating = ["👎", "👎", "-", "👍", "👍"][review.rating - 1]
comments = make_link(abs_url_for("threads.view", id=review.thread.id), "Comments")
body += f"{author} {review.rating}\n<big>{escape_hypertext(review.thread.title)}</big>\n{content}\n{comments}\n\n"
return {
"head": HEAD,

View File

@@ -124,7 +124,7 @@ def parse_forum_list_page(id, page, out, extra=None):
start = page*num_per_page+1
print(" - Fetching page {} (topics {}-{})".format(page, start, start+num_per_page), file=sys.stderr)
url = "https://forum.luanti.org/viewforum.php?f=" + str(id) + "&start=" + str(start)
url = "https://forum.minetest.net/viewforum.php?f=" + str(id) + "&start=" + str(start)
r = urllib.request.urlopen(url).read().decode("utf-8")
soup = BeautifulSoup(r, "html.parser")

View File

@@ -37,7 +37,6 @@ TEMPLATES_AUTO_RELOAD = False
LOG_SQL = False
BLOCKED_DOMAINS = []
LINK_CHECKER_IGNORED_URLS = ["liberapay.com"]
ADMIN_CONTACT_URL = ""
MONITORING_URL = None

View File

@@ -3,4 +3,4 @@
This folder only contains technical documentation for those interested in the ContentDB source code.
Documentation for using ContentDB, whether through the interface or API, is available at
<https://content.luanti.org/help/>.
<https://content.minetest.net/help/>.

View File

@@ -6,14 +6,14 @@ or for implementing ContentDB compatible servers.
## Package List API call
The client makes a single [API](https://content.luanti.org/help/api/) request to `/api/packages/`.
The client makes a single [API](https://content.minetest.net/help/api/) request to `/api/packages/`.
The query arguments will include a list of supported types, the current
[engine version](https://content.luanti.org/api/minetest_versions/),
and any hidden [Content Flags](https://content.luanti.org/help/content_flags/).
[engine version](https://content.minetest.net/api/minetest_versions/),
and any hidden [Content Flags](https://content.minetest.net/help/content_flags/).
Example URL:
<https://content.luanti.org/api/packages/?type=mod&type=game&type=txp&protocol_version=39&engine_version=5.3.0&hide=nonfree&hide=desktop_default>
<https://content.minetest.net/api/packages/?type=mod&type=game&type=txp&protocol_version=39&engine_version=5.3.0&hide=nonfree&hide=desktop_default>
Example response:
@@ -24,7 +24,7 @@ Example response:
"name": "nodecore",
"release": 1234,
"short_description": "A short description",
"thumbnail": "https://content.luanti.org/thumbnails/1/abcdef.jpg",
"thumbnail": "https://content.minetest.net/thumbnails/1/abcdef.jpg",
"title": "NodeCore",
"type": "game"
}
@@ -50,7 +50,7 @@ The client can simply download the URL mentioned in `thumbnail`.
The client downloads packages by constructing a URL for the release and downloading it:
```
https://content.luanti.org/packages/<author>/<name>/releases/<release>/download/
https://content.minetest.net/packages/<author>/<name>/releases/<release>/download/
```
This supports redirects.
@@ -104,5 +104,5 @@ response for Mobs Monster.
The client will open the package in a browser by constructing the following URL
```
https://content.luanti.org/packages/<author>/<name>/
https://content.minetest.net/packages/<author>/<name>/
```

Some files were not shown because too many files have changed in this diff Show More