Compare commits
1 Commits
policy-upd
...
game_suppo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a84ec5bad |
2
.github/SECURITY.md
vendored
2
.github/SECURITY.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest production version, deployed to <https://content.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
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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/* .
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"])
|
||||
})
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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/")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,7 @@ toc: False
|
||||
|
||||
## Rules
|
||||
|
||||
* [Terms of Service](/terms/)
|
||||
* [Rules](/rules/)
|
||||
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
|
||||
|
||||
## General Help
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
15
app/flatpages/rules.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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, [])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
214
app/markdown.py
Normal 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])
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% if version %}
|
||||
Edit {{ version.name }}
|
||||
{% else %}
|
||||
New Luanti Version
|
||||
New Minetest Version
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 © 2018-24 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
|
||||
ContentDB © 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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.") }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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?")) }}
|
||||
|
||||
|
||||
@@ -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.")) }}
|
||||
|
||||
@@ -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.") }}
|
||||
|
||||
@@ -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.") }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 > Profile > Edit signature</a>") }}
|
||||
{{ _("Go to <a href='https://forum.minetest.net/ucp.php?i=profile&mode=signature'>User Control Panel > Profile > Edit signature</a>") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user