Compare commits

...

243 Commits

Author SHA1 Message Date
rubenwardy
1ffbe93e0f Add funding.json 2025-11-06 12:22:18 +00:00
rubenwardy
0902d39970 Drop support for docker-compose v1 2025-11-01 18:27:32 +00:00
rubenwardy
8d5ba2af72 Remove unnecessary $ 2025-10-05 16:39:00 +01:00
rubenwardy
34948770ce Improve package/content technical name mismatch error 2025-10-05 16:35:08 +01:00
rubenwardy
8bafaed671 Link checker: display original href rather than resolved URL 2025-10-05 16:29:53 +01:00
rubenwardy
a604b3cd09 Move likely spammer job to worker 2025-09-23 19:35:15 +01:00
rubenwardy
ad41bc01b9 Add action to delete likely spammers 2025-09-23 19:30:43 +01:00
rubenwardy
4db70bf401 Add spam honeypot field to register form 2025-09-23 18:59:05 +01:00
rubenwardy
b88cc1366f Allow approvers and editors to see the audit log (package events only) 2025-09-23 17:59:45 +01:00
rubenwardy
feeed21b94 Hide website and donate links on the profiles of New Members 2025-09-23 16:35:46 +01:00
rubenwardy
dfa4e5a7a3 Add spam to ReportCategory 2025-09-23 16:25:39 +01:00
rubenwardy
28e5f44a30 Update translations 2025-09-23 10:00:42 +01:00
Filipino Student
b03b5b1adb Added translation using Weblate (Tagalog)
Co-authored-by: Filipino Student <hamen27545@artvara.com>
2025-09-23 10:53:55 +02:00
Ritwik
b7c6c3f338 Translated using Weblate (Hindi)
Currently translated at 0.5% (7 of 1200 strings)

Co-authored-by: Ritwik <ritwikraghav14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hi/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Pexauteau Santander
8568830cf6 Translated using Weblate (Slovak)
Currently translated at 82.5% (991 of 1200 strings)

Co-authored-by: Pexauteau Santander <pexauteau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
qwoper tite
1ac3bd1da8 Translated using Weblate (Filipino)
Currently translated at 1.1% (14 of 1200 strings)

Added translation using Weblate (Filipino)

Co-authored-by: qwoper tite <yipoli8514@inupup.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fil/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Daan sfs
9778f8be53 Translated using Weblate (Dutch)
Currently translated at 43.1% (518 of 1200 strings)

Co-authored-by: Daan sfs <xdaan736@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nl/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Nicolae Crefelean
7e47287b23 Translated using Weblate (Romanian)
Currently translated at 6.9% (83 of 1200 strings)

Co-authored-by: Nicolae Crefelean <kneekoo@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ro/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Artur Ajvazjan
7d8e45f64f Added translation using Weblate (Armenian)
Co-authored-by: Artur Ajvazjan <arturaiwasan@gmail.com>
2025-09-23 10:53:55 +02:00
Zaman Hüseynli
8f77197cde Translated using Weblate (Azerbaijani)
Currently translated at 0.1% (1 of 1200 strings)

Added translation using Weblate (Azerbaijani)

Co-authored-by: Zaman Hüseynli <zamanhuseynli23@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/az/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Денис
1f24fe0843 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Денис <denissavchenko0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Gabriel
c15e1eb042 Translated using Weblate (Romanian)
Currently translated at 6.9% (83 of 1200 strings)

Co-authored-by: Gabriel <gabrielpopa154@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ro/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Killercat103
1e6499d23a Translated using Weblate (Kurdish (Northern))
Currently translated at 0.1% (1 of 1200 strings)

Added translation using Weblate (Kurdish (Northern))

Co-authored-by: Killercat103 <killercat103@densphere.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/kmr/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Тарас Арт
19257d4dc0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Тарас Арт <fromkaniv@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Денис
762562f837 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Денис <denissavchenko0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
GiorgeGi
75192abb3a Translated using Weblate (Greek)
Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 46.4% (557 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 35.3% (424 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 35.3% (424 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 35.3% (424 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 35.3% (424 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 23.8% (286 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 23.8% (286 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 23.8% (286 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 23.8% (286 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 22.6% (272 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 22.6% (272 of 1200 strings)

Co-authored-by: GiorgeGi <giorgegi729@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
FedonT
6276dfb614 Translated using Weblate (Greek)
Currently translated at 19.3% (232 of 1200 strings)

Translated using Weblate (Greek)

Currently translated at 19.2% (231 of 1200 strings)

Co-authored-by: FedonT <phaidtheodcoc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
GiorgeGi
33860b84cf Translated using Weblate (Greek)
Currently translated at 19.2% (231 of 1200 strings)

Co-authored-by: GiorgeGi <giorgegi729@gmail.com>
Co-authored-by: GiorgeGi GiorgeGi <giorgegi729@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
nauta-turbidus
607065d94c Translated using Weblate (Polish)
Currently translated at 84.5% (1014 of 1200 strings)

Co-authored-by: nauta-turbidus <wiktor-t@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
BlackImpostor
ac02e2eb2e Translated using Weblate (Russian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Kisbenedek Márton
ce5ae3fe40 Translated using Weblate (Hungarian)
Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.5% (619 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Translated using Weblate (Hungarian)

Currently translated at 45.5% (546 of 1200 strings)

Co-authored-by: Kisbenedek Márton <martonkisbenedek@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Matin Sohrablu
fb87b51a23 Translated using Weblate (Persian)
Currently translated at 18.4% (221 of 1200 strings)

Co-authored-by: Matin Sohrablu <1matin21.1234@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fa/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
தமிழ்நேரம்
7070644842 Translated using Weblate (Tamil)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ta/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Wuzzy
ced9f320a4 Translated using Weblate (German)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Rudra Harsh
fb8264bf7d Translated using Weblate (Hindi)
Currently translated at 0.5% (6 of 1200 strings)

Co-authored-by: Rudra Harsh <harshrudra020@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hi/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
TheOnlyJoeEnderman
cfa1bee2d8 Translated using Weblate (Japanese)
Currently translated at 37.9% (455 of 1200 strings)

Co-authored-by: TheOnlyJoeEnderman <joeenderman1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
Translation: Luanti/ContentDB
2025-09-23 10:53:55 +02:00
Тарас Арт
fa11b0ffa8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (1198 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (1198 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Co-authored-by: Тарас Арт <fromkaniv@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Luanti/ContentDB
2025-09-23 10:53:54 +02:00
Ian Pedras
c3889d64a3 Translated using Weblate (Portuguese)
Currently translated at 55.0% (661 of 1200 strings)

Co-authored-by: Ian Pedras <ian@pedras.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Luanti/ContentDB
2025-09-23 10:53:54 +02:00
Zacharias Tyllström
8b69d552fd Translated using Weblate (Swedish)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Zacharias Tyllström <zacharias.tyllstrom@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
ninjum
0a1ff05c39 Translated using Weblate (Galician)
Currently translated at 9.2% (111 of 1200 strings)

Translated using Weblate (Galician)

Currently translated at 9.0% (109 of 1200 strings)

Co-authored-by: ninjum <ninhum@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/gl/
Translation: Luanti/ContentDB
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Jun Nogata
2b33a89107 Translated using Weblate (Japanese)
Currently translated at 33.4% (401 of 1200 strings)

Translated using Weblate (Japanese)

Currently translated at 31.9% (383 of 1200 strings)

Translated using Weblate (Japanese)

Currently translated at 31.8% (382 of 1200 strings)

Translated using Weblate (Japanese)

Currently translated at 30.9% (371 of 1200 strings)

Translated using Weblate (Japanese)

Currently translated at 29.5% (355 of 1200 strings)

Translated using Weblate (Japanese)

Currently translated at 26.4% (317 of 1200 strings)

Co-authored-by: Jun Nogata <nogajun@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Siber
bde76b5b46 Translated using Weblate (Turkish)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Turkish)

Currently translated at 99.6% (1196 of 1200 strings)

Co-authored-by: Siber <siber@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Денис
7c4e6aece8 Translated using Weblate (Ukrainian)
Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Co-authored-by: Денис <denissavchenko0@gmail.com>
Co-authored-by: Денис Савченко <denissavchenko0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Tanavit MINETEST
e0e50a78ed Translated using Weblate (French)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Tanavit MINETEST <tanavit@posto.ovh>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
3raven
930a460ef3 Translated using Weblate (French)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Tanavit MINETEST
f7d81f9fba Translated using Weblate (French)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Tanavit MINETEST <tanavit@posto.ovh>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
3raven
25998fdcc4 Translated using Weblate (French)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Tanavit MINETEST
617e7f043b Translated using Weblate (French)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Tanavit MINETEST <tanavit@posto.ovh>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
3raven
ced7abd27f Translated using Weblate (French)
Currently translated at 99.6% (1196 of 1200 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Tanavit MINETEST
f29a1be1eb Translated using Weblate (French)
Currently translated at 99.6% (1196 of 1200 strings)

Co-authored-by: Tanavit MINETEST <tanavit@posto.ovh>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Тарас Арт
0704ffb1a2 Translated using Weblate (Ukrainian)
Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.7% (1161 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 95.7% (1149 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.9% (1139 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 91.5% (1099 of 1200 strings)

Co-authored-by: Тарас Арт <fromkaniv@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Gg Gg
9f6c295484 Added translation using Weblate (Slovenian)
Co-authored-by: Gg Gg <veselsem0@gmail.com>
2025-09-23 10:53:54 +02:00
Nana_M
8936bdca81 Translated using Weblate (Russian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Nana_M <sab.pyrope@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
Matyáš Pilz
f86b1343b7 Translated using Weblate (Czech)
Currently translated at 99.5% (1195 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 99.5% (1195 of 1200 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2025-09-23 10:53:54 +02:00
rubenwardy
d6e39fb896 Allow editors to see reports
Some reports are about packages and not users
2025-09-23 09:52:39 +01:00
rubenwardy
4004c16504 Fix import error caused by 864add055b 2025-09-12 22:08:53 +01:00
rubenwardy
66b935037c Update private thread text 2025-09-12 21:29:50 +01:00
rubenwardy
4c78e098cf Update report closure reasons to improve clarity 2025-09-10 15:08:14 +01:00
ROllerozxa
864add055b Remove Codeberg-specific ratelimit when checking Git repos (#609) 2025-09-10 14:11:57 +01:00
rubenwardy
e44a5545a3 Add report attachments 2025-09-01 18:16:42 +01:00
rubenwardy
30aecb8565 Adjust codeberg ratelimit again 2025-09-01 08:33:02 +01:00
rubenwardy
79354453e5 Adjust codeberg ratelimit 2025-08-29 10:56:59 +01:00
rubenwardy
d6e4abec73 Fix codeberg queue ratelimit 2025-08-27 18:39:24 +01:00
rubenwardy
be03243250 Add reminder to comment in thread after invalid report resolution 2025-08-27 16:51:21 +01:00
rubenwardy
92f935c6de Add ability to search audit log by URL 2025-08-27 16:46:04 +01:00
rubenwardy
0a52e26cd0 Add report edit form 2025-08-27 16:34:11 +01:00
rubenwardy
6bf94e558a Add ability to reopen reports 2025-08-27 14:22:18 +01:00
rubenwardy
9ec215c3d4 Add last_checked_at to update config 2025-08-26 21:38:36 +01:00
rubenwardy
704c6be1c4 Add dedicated celery task for codeberg 2025-08-26 20:50:05 +01:00
rubenwardy
0adf02bf99 Reduce check_update_config ratelimit further 2025-08-26 20:33:22 +01:00
rubenwardy
8631425ff7 Add "Outdated/invalid review" report category 2025-08-26 20:26:56 +01:00
rubenwardy
b8e25f8565 Rename "Quick resolve" -> "Close report" 2025-08-26 19:35:08 +01:00
rubenwardy
394b1fe33d Disable email requirement for admins 2025-08-26 19:29:17 +01:00
rubenwardy
038e65bfe3 Add backlink to report on thread page 2025-08-26 19:29:09 +01:00
rubenwardy
6e2d8b1974 Add threads to reports 2025-08-26 19:14:47 +01:00
rubenwardy
310f1baa09 Prevent mentioned users being added to private threads 2025-08-26 18:47:02 +01:00
rubenwardy
acf9e16234 Add reports list to editor todo page 2025-08-26 18:29:14 +01:00
rubenwardy
741bd23144 Update report form text 2025-08-26 18:19:24 +01:00
rubenwardy
bdf1c2df6e Fix report category form validation 2025-08-26 18:01:11 +01:00
rubenwardy
8d1268bd19 Add report categories 2025-08-26 15:46:41 +01:00
rubenwardy
8db72faf3c Use random string ids for reports 2025-08-26 15:26:07 +01:00
rubenwardy
e4c061858e Add report received page 2025-08-26 15:21:14 +01:00
rubenwardy
0653ed2183 Small tweaks to reporting interface 2025-08-26 15:04:01 +01:00
rubenwardy
a9f82b6e1b Add reports to database 2025-08-26 14:53:44 +01:00
rubenwardy
80d06d154a Adjust check_update_config ratelimit 2025-08-25 21:49:16 +01:00
rubenwardy
6265c0665b Replace 'Minetest' with 'Luanti' in remaining places 2025-08-25 14:21:43 +01:00
rubenwardy
1c2a56e784 Fix failing CI 2025-08-25 13:53:31 +01:00
rubenwardy
e03c8f04e1 Fix crash in approval stats 2025-08-24 22:10:56 +01:00
rubenwardy
bd23a99aec Update storage table titles 2025-08-24 22:08:14 +01:00
rubenwardy
26272ce793 Add 60/m ratelimit to check_update_config task 2025-08-24 21:40:44 +01:00
rubenwardy
66f918c1bf Fix crash on create package error due to missing rollback 2025-08-16 16:45:30 +01:00
rubenwardy
a6fccc7c58 Fix unknown error on setting null package field 2025-07-23 22:11:18 +01:00
rubenwardy
ae05c10e7c Remove margin above first item in release notes 2025-07-17 23:09:22 +01:00
rubenwardy
14d8ef6cb1 Add height limit to release notes side card 2025-07-17 23:05:03 +01:00
rubenwardy
bc62c2b1be Increase zipgrep per-zip timeout 2025-07-05 22:07:58 +01:00
rubenwardy
dedafe9c71 Require screenshot for all packages 2025-07-04 21:37:52 +01:00
rubenwardy
e754d8d80d Update package inclusion policy (#601)
Fixes #566, fixes #222
2025-07-03 12:16:11 +01:00
rubenwardy
cbb59e5e55 Fix GitLab webhook repo not matching with capital letters 2025-07-02 23:01:57 +01:00
rubenwardy
1928a2302c Improve storage page queries 2025-07-02 21:06:09 +01:00
rubenwardy
6b3a2a0fe7 Fix storage page by storing file sizes in the db 2025-07-02 20:47:30 +01:00
rubenwardy
52802f44f6 Update Python dependencies 2025-07-02 20:12:02 +01:00
rubenwardy
f01adc4cb4 Fix failing CI 2025-07-02 20:02:27 +01:00
rubenwardy
c21dc5313d Improve header anchor link styling 2025-07-01 21:05:13 +01:00
Wuzzy
5243176d74 Use the Luanti name in places where forgotten (#600) 2025-07-01 20:47:47 +01:00
rubenwardy
c495fcbd1a Reject new packages with depends.txt or description.txt 2025-07-01 20:18:29 +01:00
rubenwardy
036a55e61e Fix broken link and aside in WTFPL help page
Fixes #598
2025-07-01 20:16:12 +01:00
rubenwardy
21ef5f9b84 Fix heading anchors and add anchor links
Fixes #460
2025-06-19 18:40:26 +01:00
rubenwardy
2ddcbfb5ab Fix crash on unknown code language 2025-06-04 18:13:59 +01:00
rubenwardy
c931c78b6a Disable HTML sanitisation on help pages 2025-06-03 23:14:42 +01:00
rubenwardy
815d812297 Re-enable Bleach linkify to add rel=nofollow 2025-06-03 23:10:07 +01:00
rubenwardy
8ed86b53ca Switch to markdown-it (#595)
Fixes #537, fixes #586
2025-06-03 22:41:20 +01:00
rubenwardy
98f27364f2 Clean up polltask.js 2025-05-01 17:04:27 +01:00
rubenwardy
4e502f38aa zipgrep: Add ability to filter by package type 2025-04-27 18:08:42 +01:00
rubenwardy
3a468a9b85 Fix zipgrep timeout 2025-04-27 18:05:51 +01:00
rubenwardy
a6009654c7 Add missing log in zipgrep 2025-04-27 17:54:47 +01:00
rubenwardy
2d8660902d Add per-package timeout to zipgrep and simplify code 2025-04-27 17:53:44 +01:00
rubenwardy
2e5ced23a8 Add progress bar to zipgrep 2025-04-27 17:35:09 +01:00
rubenwardy
4011cc56b6 Add task status to tasks page 2025-04-27 17:09:26 +01:00
ROllerozxa
1543965e5f Use package metadata translations in spotlight carousel (#577) 2025-04-27 14:17:09 +01:00
rubenwardy
c21a56585f Update github_username on sign in 2025-04-13 15:44:13 +01:00
rubenwardy
cd53696831 Fix not being able to disconnect GitHub accounts 2025-04-13 15:39:48 +01:00
rubenwardy
8777d2bfd3 Remove include_images from /for-client/reviews/ doc 2025-03-30 15:05:42 +01:00
rubenwardy
00cf79224d Document /for-client/reviews/ 2025-03-30 15:04:58 +01:00
rubenwardy
5b0d42173f Add terms of service to comply with Online Safety Act 2023 (#578) 2025-03-09 13:48:03 +00:00
rubenwardy
6891ee8b19 Remove ability to create private threads except for approval threads 2025-03-07 19:31:38 +00:00
rubenwardy
5f2b2ffdf1 Require screenshot approval for non-trusted members 2025-03-05 20:19:18 +00:00
rubenwardy
f0e67c93d6 Prohibit illegal drugs 2025-03-04 14:25:35 +00:00
rubenwardy
e5fd908b54 Fix bad links in Tamil translation 2025-02-11 18:43:52 +00:00
rubenwardy
3af7a19563 Enable Czech and Tamil languages 2025-02-11 18:37:15 +00:00
ninjum
e1f0792dce Translated using Weblate (Galician)
Currently translated at 7.2% (87 of 1200 strings)

Co-authored-by: ninjum <ninhum@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/gl/
Translation: Minetest/ContentDB
2025-02-08 23:00:46 +01:00
Ilia
2e91656245 Translated using Weblate (Persian)
Currently translated at 18.0% (217 of 1200 strings)

Co-authored-by: Ilia <iliaabbasi@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fa/
Translation: Minetest/ContentDB
2025-02-08 23:00:46 +01:00
Тарас Арт
8378095343 Translated using Weblate (Ukrainian)
Currently translated at 82.2% (987 of 1200 strings)

Translated using Weblate (Ukrainian)

Currently translated at 77.6% (932 of 1200 strings)

Co-authored-by: Тарас Арт <fromkaniv@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2025-02-08 23:00:46 +01:00
Balázs Kovács
b6f67d4b0e Translated using Weblate (Hungarian)
Currently translated at 37.9% (455 of 1200 strings)

Co-authored-by: Balázs Kovács <kovacs.balazs.ktk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2025-02-08 23:00:46 +01:00
Miguel
ffe808c915 Translated using Weblate (Spanish)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Miguel <mp0187595@tutamail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2025-02-08 23:00:46 +01:00
தமிழ்நேரம்
6169f4c0e4 Translated using Weblate (Tamil)
Currently translated at 100.0% (1200 of 1200 strings)

Added translation using Weblate (Tamil)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ta/
Translation: Minetest/ContentDB
2025-02-08 23:00:45 +01:00
Poesty Li
8c5e542268 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Poesty Li <poesty7450@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2025-02-08 23:00:45 +01:00
reimu105
893b902314 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 37.6% (452 of 1200 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 37.4% (449 of 1200 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 37.2% (447 of 1200 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 36.7% (441 of 1200 strings)

Co-authored-by: reimu105 <peter112548@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2025-02-08 23:00:45 +01:00
Siber
2435e8e3d0 Translated using Weblate (Turkish)
Currently translated at 96.9% (1163 of 1200 strings)

Translated using Weblate (Turkish)

Currently translated at 96.9% (1163 of 1200 strings)

Translated using Weblate (Turkish)

Currently translated at 94.8% (1138 of 1200 strings)

Translated using Weblate (Turkish)

Currently translated at 82.9% (995 of 1200 strings)

Co-authored-by: Siber <anonloxu@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2025-02-08 23:00:45 +01:00
jolesh
628d44460d Translated using Weblate (Esperanto)
Currently translated at 15.5% (187 of 1200 strings)

Co-authored-by: jolesh <jolesh0815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translation: Minetest/ContentDB
2025-02-08 23:00:45 +01:00
Tanavit MINETEST
fc1b7e500d Translated using Weblate (French)
Currently translated at 99.5% (1194 of 1200 strings)

Translated using Weblate (French)

Currently translated at 99.5% (1194 of 1200 strings)

Co-authored-by: Tanavit MINETEST <tanavit@posto.ovh>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
ats
7cfbbbe7e6 Translated using Weblate (Portuguese)
Currently translated at 54.9% (659 of 1200 strings)

Co-authored-by: ats <athos2256@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
Stepan Bazrov
bea743b536 Translated using Weblate (Russian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Stepan Bazrov <bazrovstepan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
Jorge Rodríguez
6ccc575cb4 Translated using Weblate (Spanish)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (1196 of 1200 strings)

Co-authored-by: Jorge Rodríguez <mr.jrodriguez05@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
Muhammad Nuruddin
9e4be57754 Translated using Weblate (Malay)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Muhammad Nuruddin <nuruddin6106@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
Vasilis Sarantidis
82b47628ae Translated using Weblate (Greek)
Currently translated at 18.2% (219 of 1200 strings)

Co-authored-by: Vasilis Sarantidis <bilsarantidis@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
Translation: Minetest/ContentDB
2025-02-08 23:00:44 +01:00
Oleg
3bd62f4184 Translated using Weblate (Ukrainian)
Currently translated at 69.3% (832 of 1200 strings)

Co-authored-by: Oleg <bauyrakoleg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
ROllerozxa
9a98c2f6c2 Translated using Weblate (Swedish)
Currently translated at 96.6% (1160 of 1200 strings)

Translated using Weblate (Swedish)

Currently translated at 91.7% (1101 of 1200 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
Tancrède
73c1706e6a Translated using Weblate (French)
Currently translated at 96.2% (1155 of 1200 strings)

Co-authored-by: Tancrède <tancrede.meulien@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
Just Playing
649ee8bcd6 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Just Playing <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
Matyáš Pilz
37e8f2dc28 Translated using Weblate (Czech)
Currently translated at 99.5% (1195 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 95.4% (1145 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 88.2% (1059 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 85.6% (1028 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 85.5% (1026 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 84.2% (1011 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 83.0% (996 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 78.8% (946 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 78.2% (939 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 76.6% (920 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 76.3% (916 of 1200 strings)

Translated using Weblate (Czech)

Currently translated at 70.4% (845 of 1200 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
BlackImpostor
4d9628a156 Translated using Weblate (Russian)
Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2025-02-08 23:00:43 +01:00
Wuzzy
0ff9a3838e Translated using Weblate (German)
Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1200 of 1200 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1200 of 1200 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2025-02-08 23:00:42 +01:00
rubenwardy
24eacb191d Fix crash in package_view_client when long desc is null 2025-01-26 16:14:22 +00:00
rubenwardy
23e9ad6ef5 Fix crash due to faulty engine version wildcard 2025-01-26 16:05:47 +00:00
rubenwardy
c7f26f706d Change wording of reviews hypertext helpfulness 2024-12-24 09:29:33 +00:00
rubenwardy
699eabef80 Fix statistics reporting the 5.10 client as web (not just the import script) 2024-11-27 02:29:08 +00:00
ROllerozxa
73376194e0 Fix statistics reporting the 5.10 client as web (#568) 2024-11-22 19:02:36 +00:00
rubenwardy
aafa56df95 Update reviews hypertext 2024-11-15 18:57:31 +00:00
rubenwardy
978c5d9704 Update translations 2024-11-10 15:53:39 +00:00
chocomint
c332e8f940 Translated using Weblate (Spanish)
Currently translated at 100.0% (1197 of 1197 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
2024-11-10 15:52:11 +00:00
rubenwardy
f116259f6a Update translations 2024-11-03 14:22:42 +00:00
chocomint
3f9902b001 Translated using Weblate (Spanish)
Currently translated at 94.1% (1127 of 1197 strings)

Co-authored-by: chocomint <silentxe1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Jakub Z
a627276ab4 Translated using Weblate (Polish)
Currently translated at 84.4% (1011 of 1197 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Unacceptium
f72a66816a Translated using Weblate (Hungarian)
Currently translated at 37.9% (454 of 1197 strings)

Translated using Weblate (Hungarian)

Currently translated at 37.5% (450 of 1197 strings)

Co-authored-by: Unacceptium <unacceptium@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Honzapkcz
508f7d7e2b Translated using Weblate (Czech)
Currently translated at 61.6% (738 of 1197 strings)

Translated using Weblate (Czech)

Currently translated at 61.5% (737 of 1197 strings)

Co-authored-by: Honzapkcz <honzapkc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Matyáš Pilz
b86d372bd2 Translated using Weblate (Czech)
Currently translated at 68.7% (823 of 1197 strings)

Translated using Weblate (Czech)

Currently translated at 68.6% (822 of 1197 strings)

Translated using Weblate (Czech)

Currently translated at 61.2% (733 of 1197 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
BlackImpostor
c75fd51626 Translated using Weblate (Russian)
Currently translated at 100.0% (1197 of 1197 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (1197 of 1197 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Wuzzy
6ad12288c3 Translated using Weblate (German)
Currently translated at 100.0% (1197 of 1197 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Muhammad Rifqi Priyo Susanto
af2543a99e Translated using Weblate (Indonesian)
Currently translated at 100.0% (1197 of 1197 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
2c61032d15 Translated using Weblate (Malay)
Currently translated at 100.0% (1197 of 1197 strings)

Co-authored-by: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi <translation@mnh48.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2024-11-03 15:21:48 +01:00
rubenwardy
a54104aa82 Add forum_url to API, change to forum.luanti.org 2024-11-03 14:21:09 +00:00
rubenwardy
dd2e73b40f Change content.minetest.net to content.luanti.org 2024-10-29 22:53:28 +00:00
rubenwardy
a5ac4f38cf Add unique release name check 2024-10-28 22:50:27 +00:00
rubenwardy
2ff11dec0a Update translations 2024-10-15 21:47:13 +01:00
gallegonovato
8e1547ca3b Translated using Weblate (Spanish)
Currently translated at 93.2% (1114 of 1194 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-10-15 22:38:56 +02:00
Jorge Rodríguez
757e182d1b Translated using Weblate (Spanish)
Currently translated at 93.2% (1113 of 1194 strings)

Co-authored-by: Jorge Rodríguez <mr.jrodriguez05@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-10-15 22:38:56 +02:00
gallegonovato
5562ca6039 Translated using Weblate (Spanish)
Currently translated at 93.2% (1113 of 1194 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-10-15 22:38:56 +02:00
Jorge Rodríguez
74cf577245 Translated using Weblate (Spanish)
Currently translated at 86.5% (1034 of 1194 strings)

Co-authored-by: Jorge Rodríguez <mr.jrodriguez05@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-10-15 22:38:56 +02:00
Matyáš Pilz
79387309d8 Translated using Weblate (Czech)
Currently translated at 56.3% (673 of 1194 strings)

Translated using Weblate (Czech)

Currently translated at 54.2% (648 of 1194 strings)

Translated using Weblate (Czech)

Currently translated at 48.0% (574 of 1194 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2024-10-15 22:38:55 +02:00
Pexauteau Santander
e4b81feb5c Translated using Weblate (Slovak)
Currently translated at 83.2% (994 of 1194 strings)

Translated using Weblate (Slovak)

Currently translated at 77.4% (925 of 1194 strings)

Co-authored-by: Pexauteau Santander <pexauteau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Minetest/ContentDB
2024-10-15 22:38:55 +02:00
3raven
58ac57e098 Translated using Weblate (French)
Currently translated at 96.6% (1154 of 1194 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-10-15 22:38:55 +02:00
ROllerozxa
abc2941756 Minetest -> Luanti (#564) 2024-10-15 21:36:50 +01:00
rubenwardy
1432384b63 Review hypertext: Fix gap before comments link 2024-10-08 13:54:30 +01:00
rubenwardy
52df207088 Review hypertext: Fix links in reviews not working 2024-10-08 13:52:08 +01:00
rubenwardy
7f834dbf8c Review hypertext: Fix incorrect icons 2024-10-08 13:43:19 +01:00
rubenwardy
9131b29b48 Review hypertext: Add no reviews message 2024-10-08 13:40:58 +01:00
rubenwardy
f621cd13d2 Review hypertext: Use placeholder element for icons 2024-10-08 13:24:20 +01:00
rubenwardy
69904dbe81 API: Sort tags and ContentWarnings by name 2024-10-06 21:03:37 +01:00
rubenwardy
d56430c0f0 Allow Discord webhook URLs to be an array 2024-10-06 21:03:13 +01:00
rubenwardy
f69bc8fc1e Fix ungraceful error on non-unique OAuthClient title 2024-09-29 13:35:49 +01:00
rubenwardy
5a173ee18b Fix another unchecked watcher append 2024-09-28 19:13:10 +01:00
rubenwardy
6429b2e26d Fix crash on mention on new thread 2024-09-28 19:07:32 +01:00
wsor4035
93f36adfea Fix missing zipgrep on Alpine (#562) 2024-09-28 17:41:01 +01:00
Francesco Rossi
25547c9f38 Translated using Weblate (Italian)
Currently translated at 100.0% (1194 of 1194 strings)

Co-authored-by: Francesco Rossi <zhu.gamedev@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-09-21 14:33:57 +02:00
Francesco Rossi
6425149d20 Translated using Weblate (Italian)
Currently translated at 76.8% (917 of 1194 strings)

Co-authored-by: Francesco Rossi <zhu.gamedev@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
Giov4
4738e11ed0 Translated using Weblate (Italian)
Currently translated at 74.4% (889 of 1194 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
Muhammad Rifqi Priyo Susanto
ae67f6ce79 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1194 of 1194 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
BlackImpostor
c23f004d35 Translated using Weblate (Russian)
Currently translated at 100.0% (1194 of 1194 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
Wuzzy
8effec2cbb Translated using Weblate (German)
Currently translated at 100.0% (1194 of 1194 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
gallegonovato
5afc429c25 Translated using Weblate (Spanish)
Currently translated at 86.0% (1027 of 1194 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
John Donne
d5552ad517 Translated using Weblate (French)
Currently translated at 91.6% (1094 of 1194 strings)

Translated using Weblate (French)

Currently translated at 91.0% (1087 of 1194 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-09-20 10:24:28 +02:00
rubenwardy
65a14ffdf1 Return JSON for collection based on Accept header
This will be used in the future to install collections inside Minetest
2024-09-15 14:35:23 +01:00
rubenwardy
837d0b5bc1 Link Checker: Allow 403 status codes
Cloudflare likes to break the Internet, so we'll have to ignore
403 errors from sites in the link checker.
2024-09-05 19:19:16 +01:00
rubenwardy
5b1417f432 Remove game support dependency cycle error message 2024-08-31 13:54:29 +01:00
rubenwardy
53a004c41c Remove unused experimental /api/welcome/v1/ API 2024-08-26 12:01:51 +01:00
rubenwardy
ac34939c99 Remove normalization of trailing line endings 2024-08-26 11:56:30 +01:00
rubenwardy
9aa8886309 Fix edit audit log entries being created for no changes 2024-08-26 11:53:13 +01:00
rubenwardy
1166cca357 Update translations 2024-08-26 00:50:05 +01:00
Čarvanavoki
395d3dd16b Added translation using Weblate (Belarusian)
Co-authored-by: Čarvanavoki <dmega5941@gmail.com>
2024-08-26 01:29:32 +02:00
José Douglas
009dfd07de Translated using Weblate (Portuguese)
Currently translated at 51.2% (610 of 1191 strings)

Co-authored-by: José Douglas <josedouglas20002014@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
ninjum
ff07ff5b7f Translated using Weblate (Galician)
Currently translated at 5.2% (62 of 1191 strings)

Co-authored-by: ninjum <ninhum@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/gl/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
Just Playing
2b32cfe6fa Translated using Weblate (Indonesian)
Currently translated at 100.0% (1191 of 1191 strings)

Co-authored-by: Just Playing <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
hugoalh
b31e9e71b6 Translated using Weblate (Chinese (Traditional))
Currently translated at 36.7% (438 of 1191 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 34.8% (415 of 1191 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 32.1% (383 of 1191 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 28.2% (337 of 1191 strings)

Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
Muhammad Rifqi Priyo Susanto
94bf1973a0 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1191 of 1191 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
John Donne
357dfe76e8 Translated using Weblate (French)
Currently translated at 90.4% (1077 of 1191 strings)

Translated using Weblate (French)

Currently translated at 89.0% (1060 of 1191 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
BlackImpostor
5da955b3a5 Translated using Weblate (Russian)
Currently translated at 100.0% (1191 of 1191 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
Athozus
7584a867eb Translated using Weblate (French)
Currently translated at 88.5% (1055 of 1191 strings)

Translated using Weblate (French)

Currently translated at 82.2% (980 of 1191 strings)

Translated using Weblate (French)

Currently translated at 82.1% (979 of 1191 strings)

Co-authored-by: Athozus <athozus@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-08-26 01:29:32 +02:00
rubenwardy
9c77212f4a Filter for supported languages before parsing .tr files 2024-08-26 00:20:49 +01:00
rubenwardy
2b62224a5b Redirect to reports after attempting to recreate package 2024-08-26 00:17:36 +01:00
rubenwardy
bb561104f8 Add message to removal page about editing packages 2024-08-26 00:17:14 +01:00
rubenwardy
bdd9ab6a29 Add protonmail unsupported message 2024-08-16 22:12:25 +01:00
rubenwardy
d450d6bae3 Fix CI 2024-08-10 13:53:56 +01:00
rubenwardy
02cc464098 Switch to Alpine-based docker images 2024-08-10 13:52:02 +01:00
rubenwardy
563345eddd Hide privacy policy updated message 2024-08-10 13:50:32 +01:00
rubenwardy
e5e3230a16 Fix ungraceful error on BadZipFile 2024-08-05 12:11:44 +01:00
y5nw
ed4d4c67d9 Translated using Weblate (Chinese (Simplified))
Currently translated at 79.2% (944 of 1191 strings)

Co-authored-by: y5nw <y5nw@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-08-01 01:32:10 +02:00
Giov4
42df276e73 Translated using Weblate (Italian)
Currently translated at 74.5% (888 of 1191 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-08-01 01:32:10 +02:00
BlackImpostor
bd17080f2a Translated using Weblate (Russian)
Currently translated at 100.0% (1191 of 1191 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (1191 of 1191 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-08-01 01:32:09 +02:00
Wuzzy
2fb7ddaaee Translated using Weblate (German)
Currently translated at 100.0% (1191 of 1191 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-08-01 01:32:09 +02:00
rubenwardy
ef868f776c Add LINK_CHECKER_IGNORED_URLS setting 2024-07-29 23:16:47 +01:00
rubenwardy
06979345c7 Fix thumbnails having incorrect mimetypes
Fixes #555
2024-07-27 22:17:04 +01:00
rubenwardy
e5de270e65 Fix short_description being cut when not needed 2024-07-27 22:10:04 +01:00
rubenwardy
031c3c4684 Fix incorrect message when marking thread as Changes Needed 2024-07-23 22:23:18 +01:00
rubenwardy
a9d31590e8 Fix issue with anonymous reports 2024-07-23 22:19:05 +01:00
rubenwardy
4c4a55872a Update translations 2024-07-23 22:12:38 +01:00
ssdaniel24
9387db5f8d Translated using Weblate (Russian)
Currently translated at 100.0% (1182 of 1182 strings)

Co-authored-by: ssdaniel24 <bo7oaonteg2m@mail.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-07-23 23:12:09 +02:00
BlackImpostor
6a5c7d44bf Translated using Weblate (Russian)
Currently translated at 100.0% (1182 of 1182 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-07-23 23:12:09 +02:00
y5nw
f1ace7fce8 Translated using Weblate (Chinese (Simplified))
Currently translated at 79.9% (945 of 1182 strings)

Co-authored-by: y5nw <y5nw@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-07-23 23:12:09 +02:00
Just Playing
20c946d127 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1182 of 1182 strings)

Co-authored-by: Just Playing <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-07-23 23:12:09 +02:00
rubenwardy
bb2a1f3638 Add report button to account deletion section 2024-07-23 22:11:48 +01:00
rubenwardy
e603f29b47 Translations: Add specific help for games 2024-07-18 08:01:46 +01:00
rubenwardy
9c2ecd1e22 Show translation template if there aren't any content translations 2024-07-18 07:25:32 +01:00
rubenwardy
80c3416ca7 Fix issue with check all zip files 2024-07-09 00:14:04 +01:00
208 changed files with 114472 additions and 54533 deletions

2
.github/SECURITY.md vendored
View File

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

View File

@@ -6,6 +6,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install docker-compose
run: sudo apt-get install -y docker-compose
- uses: actions/checkout@v4
- name: Copy config
run: cp utils/ci/* .

View File

@@ -1,16 +1,20 @@
FROM python:3.10.11
FROM python:3.10.11-alpine
RUN groupadd -g 5123 cdb && \
useradd -r -u 5123 -g cdb cdb
RUN addgroup --gid 5123 cdb && \
adduser --uid 5123 -S cdb -G cdb
WORKDIR /home/cdb
RUN \
apk add --no-cache postgresql-libs git bash unzip && \
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev g++
RUN mkdir /var/cdb
RUN chown -R cdb:cdb /var/cdb
COPY requirements.lock.txt requirements.lock.txt
RUN pip install -r requirements.lock.txt
RUN pip install gunicorn
RUN pip install -r requirements.lock.txt && \
pip install gunicorn
COPY utils utils
COPY config.cfg config.cfg

View File

@@ -1,7 +1,7 @@
# ContentDB
![Build Status](https://github.com/minetest/contentdb/actions/workflows/test.yml/badge.svg)
![Build Status](https://github.com/luanti-org/contentdb/actions/workflows/test.yml/badge.svg)
A content database for Minetest mods, games, and more.\
A content database for Luanti mods, games, and more.\
Developed by rubenwardy, license AGPLv3.0+.
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
@@ -82,7 +82,7 @@ Package "1" --> "*" Release
Package "1" --> "*" Dependency
Package "1" --> "*" Tag
Package "1" --> "*" MetaPackage : provides
Release --> MinetestVersion
Release --> LuantiVersion
Package --> License
Dependency --> Package
Dependency --> MetaPackage

View File

@@ -21,13 +21,12 @@ import redis
from flask import redirect, url_for, render_template, flash, request, Flask, send_from_directory, make_response, render_template_string
from flask_babel import Babel, gettext
from flask_flatpages import FlatPages
from flask_flatpages.utils import pygmented_markdown
from flask_github import GitHub
from flask_login import logout_user, current_user, LoginManager
from flask_mail import Mail
from flask_wtf.csrf import CSRFProtect
from app.markdown import init_markdown, MARKDOWN_EXTENSIONS, MARKDOWN_EXTENSION_CONFIG
from app.markdown import init_markdown, render_markdown
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
@@ -67,19 +66,18 @@ app = Flask(__name__, static_folder="public/static")
def my_flatpage_renderer(text):
# Render with jinja first
prerendered_body = render_template_string(text)
return pygmented_markdown(prerendered_body, flatpages=pages)
return render_markdown(prerendered_body, clean=False)
app.config["FLATPAGES_ROOT"] = "flatpages"
app.config["FLATPAGES_EXTENSION"] = ".md"
app.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = MARKDOWN_EXTENSIONS
app.config["FLATPAGES_EXTENSION_CONFIG"] = MARKDOWN_EXTENSION_CONFIG
app.config["FLATPAGES_HTML_RENDERER"] = my_flatpage_renderer
app.config["WTF_CSRF_TIME_LIMIT"] = None
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "../translations"
app.config["LANGUAGES"] = {
"en": "English",
"cs": "čeština",
"de": "Deutsch",
"es": "Español",
"fr": "Français",
@@ -90,6 +88,7 @@ app.config["LANGUAGES"] = {
"ru": "русский язык",
"sk": "Slovenčina",
"sv": "Svenska",
"ta": "தமிழ்",
"tr": "Türkçe",
"uk": "Українська",
"vi": "tiếng Việt",

View File

@@ -1,248 +1,252 @@
# THIS FILE IS AUTOGENERATED: utils/extract_translations.py
from flask_babel import gettext
from flask_babel import pgettext
# NOTE: tags: title for server_tools
pgettext("tags", "Server Moderation and Tools")
# NOTE: tags: description for server_tools
pgettext("tags", "Helps with server maintenance and moderation")
# NOTE: tags: title for pvp
pgettext("tags", "Player vs Player (PvP)")
# NOTE: tags: description for pvp
pgettext("tags", "Designed to be played competitively against other players")
# NOTE: tags: title for player_effects
pgettext("tags", "Player Effects / Power Ups")
# NOTE: tags: description for player_effects
pgettext("tags", "For content that changes player effects, including physics, for example: speed, jump height or gravity.")
# NOTE: tags: title for jam_game_2023
pgettext("tags", "Jam / Game 2023")
# NOTE: tags: description for jam_game_2023
pgettext("tags", "Entries to the 2023 Minetest Game Jam ")
# NOTE: tags: title for mapgen
pgettext("tags", "Mapgen / Biomes / Decoration")
# NOTE: tags: description for mapgen
pgettext("tags", "New mapgen or changes mapgen")
# NOTE: tags: title for world_tools
pgettext("tags", "World Maintenance and Tools")
# NOTE: tags: description for world_tools
pgettext("tags", "Tools to manage the world")
# NOTE: tags: title for inventory
pgettext("tags", "Inventory")
# NOTE: tags: description for inventory
pgettext("tags", "Changes the inventory GUI")
# NOTE: tags: title for developer_tools
pgettext("tags", "Developer Tools")
# NOTE: tags: description for developer_tools
pgettext("tags", "Tools for game and mod developers")
# NOTE: tags: title for april_fools
pgettext("tags", "Joke")
# NOTE: tags: description for april_fools
pgettext("tags", "For humorous content, meant as a novelty or joke, not to be taken seriously, and that is not meant to be used seriously or long-term.")
# NOTE: tags: title for singleplayer
pgettext("tags", "Singleplayer-focused")
# NOTE: tags: description for singleplayer
pgettext("tags", "Content that can be played alone")
# NOTE: tags: title for crafting
pgettext("tags", "Crafting")
# NOTE: tags: description for crafting
pgettext("tags", "Big changes to crafting gameplay")
# NOTE: tags: title for adventure__rpg
pgettext("tags", "Adventure / RPG")
# NOTE: tags: title for shooter
pgettext("tags", "Shooter")
# NOTE: tags: description for shooter
pgettext("tags", "First person shooters (FPS) and more")
# NOTE: tags: title for sound_music
pgettext("tags", "Sounds / Music")
# NOTE: tags: description for sound_music
pgettext("tags", "Focuses on or adds new sounds or musical things")
# NOTE: tags: title for tools
pgettext("tags", "Tools / Weapons / Armor")
# NOTE: tags: description for tools
pgettext("tags", "Adds or changes tools, weapons, and armor")
# NOTE: tags: title for plants_and_farming
pgettext("tags", "Plants and Farming")
# NOTE: tags: description for plants_and_farming
pgettext("tags", "Adds new plants or other farmable resources.")
# NOTE: tags: title for simulation
pgettext("tags", "Sims")
# NOTE: tags: description for simulation
pgettext("tags", "Mods and games that aim to simulate real life activity. Similar to SimCity/The Sims/OpenTTD/etc.")
# NOTE: tags: title for chat
pgettext("tags", "Chat / Commands")
# NOTE: tags: description for chat
pgettext("tags", "Focus on player chat/communication or console interaction.")
# NOTE: tags: title for multiplayer
pgettext("tags", "Multiplayer-focused")
# NOTE: tags: description for multiplayer
pgettext("tags", "Can/should only be used in multiplayer")
# NOTE: tags: title for complex_installation
pgettext("tags", "Complex installation")
# NOTE: tags: description for complex_installation
pgettext("tags", "Requires futher installation steps, such as installing LuaRocks or editing the trusted mod setting")
# NOTE: tags: title for magic
pgettext("tags", "Magic / Enchanting")
# NOTE: tags: title for building_mechanics
pgettext("tags", "Building Mechanics and Tools")
# NOTE: tags: description for building_mechanics
pgettext("tags", "Adds game mechanics or tools that change how players build.")
# NOTE: tags: title for oneofakind__original
pgettext("tags", "One-of-a-kind / Original")
# NOTE: tags: description for oneofakind__original
pgettext("tags", "For games and such that are of their own kind, distinct and original in nature to others of the same category.")
# NOTE: tags: title for building
pgettext("tags", "Building")
# NOTE: tags: description for building
pgettext("tags", "Focuses on building, such as adding new materials or nodes")
# NOTE: tags: title for decorative
pgettext("tags", "Decorative")
# NOTE: tags: description for decorative
pgettext("tags", "Adds nodes with no other purpose than for use in building")
# NOTE: tags: title for puzzle
pgettext("tags", "Puzzle")
# NOTE: tags: description for puzzle
pgettext("tags", "Focus on puzzle solving instead of combat")
# NOTE: tags: title for environment
pgettext("tags", "Environment / Weather")
# NOTE: tags: description for environment
pgettext("tags", "Improves the world, adding weather, ambient sounds, or other environment mechanics")
# NOTE: tags: title for less_than_px
pgettext("tags", "<16px")
# NOTE: tags: description for less_than_px
pgettext("tags", "Less than 16px")
# NOTE: tags: title for mobs
pgettext("tags", "Mobs / Animals / NPCs")
# NOTE: tags: description for mobs
pgettext("tags", "Adds mobs, animals, and non-player characters")
# NOTE: tags: title for custom_mapgen
pgettext("tags", "Custom mapgen")
# NOTE: tags: description for custom_mapgen
pgettext("tags", "Contains a completely custom mapgen implemented in Lua, usually requires worlds to be set to the 'singlenode' mapgen.")
# NOTE: tags: title for survival
pgettext("tags", "Survival")
# NOTE: tags: description for survival
pgettext("tags", "Written specifically for survival gameplay with a focus on game-balance, difficulty level, or resources available through crafting, mining, ...")
# NOTE: tags: title for education
pgettext("tags", "Education")
# NOTE: tags: description for education
pgettext("tags", "Either has educational value, or is a tool to help teachers ")
# NOTE: tags: title for storage
pgettext("tags", "Storage")
# NOTE: tags: description for storage
pgettext("tags", "Adds or improves item storage mechanics")
# NOTE: tags: title for library
pgettext("tags", "API / Library")
# NOTE: tags: description for library
pgettext("tags", "Primarily adds an API for other mods to use")
# NOTE: tags: title for hud
pgettext("tags", "HUD")
# NOTE: tags: description for hud
pgettext("tags", "For mods that grant the player extra information in the HUD")
# NOTE: tags: title for mini-game
pgettext("tags", "Mini-game")
# NOTE: tags: description for mini-game
pgettext("tags", "Adds a mini-game to be played within Minetest")
# NOTE: tags: title for pve
pgettext("tags", "Player vs Environment (PvE)")
# NOTE: tags: description for pve
pgettext("tags", "For content designed for one or more players that focus on combat against the world, mobs, or NPCs.")
# NOTE: tags: title for sports
pgettext("tags", "Sports")
# NOTE: tags: title for 16px
pgettext("tags", "16px")
# NOTE: tags: description for 16px
pgettext("tags", "For 16px texture packs")
# NOTE: tags: title for 64px
pgettext("tags", "64px")
# NOTE: tags: description for 64px
pgettext("tags", "For 64px texture packs")
# NOTE: tags: title for transport
pgettext("tags", "Transport")
# NOTE: tags: description for transport
pgettext("tags", "Adds or changes transportation methods. Includes teleportation, vehicles, ridable mobs, transport infrastructure and thematic content")
# NOTE: tags: title for seasonal
pgettext("tags", "Seasonal")
# NOTE: tags: description for seasonal
pgettext("tags", "For content generally themed around a certain season or holiday")
# NOTE: tags: title for food
pgettext("tags", "Food / Drinks")
# NOTE: tags: title for commerce
pgettext("tags", "Commerce / Economy")
# NOTE: tags: description for commerce
pgettext("tags", "Related to economies, money, and trading")
# NOTE: tags: title for gui
pgettext("tags", "GUI")
# NOTE: tags: description for gui
pgettext("tags", "For content whose main utility or features are provided within a GUI, on-screen menu, or similar")
# NOTE: tags: title for jam_combat_mod
pgettext("tags", "Jam / Combat 2020")
# NOTE: tags: description for jam_combat_mod
pgettext("tags", "For mods created for the Discord \"Combat\" modding event in 2020")
# NOTE: tags: title for mtg
pgettext("tags", "Minetest Game improved")
# NOTE: tags: description for mtg
pgettext("tags", "Forks of Minetest Game")
# NOTE: tags: title for jam_weekly_2021
pgettext("tags", "Jam / Weekly Challenges 2021")
# NOTE: tags: description for jam_weekly_2021
pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding event in 2021")
# NOTE: tags: title for creative
pgettext("tags", "Creative")
# NOTE: tags: description for creative
pgettext("tags", "Written specifically or exclusively for use in creative mode. Adds content only available through a creative inventory, or provides tools that facilitate ingame creation and doesn't add difficulty or scarcity")
# NOTE: tags: title for technology
pgettext("tags", "Machines / Electronics")
# NOTE: tags: description for technology
pgettext("tags", "Adds machines useful in automation, tubes, or power.")
# NOTE: tags: title for 32px
pgettext("tags", "32px")
# NOTE: tags: description for 32px
pgettext("tags", "For 32px texture packs")
# NOTE: tags: title for strategy_rts
pgettext("tags", "Strategy / RTS")
# NOTE: tags: description for strategy_rts
pgettext("tags", "Games and mods with a heavy strategy component, whether real-time or turn-based")
# NOTE: tags: title for skins
pgettext("tags", "Player customization / Skins")
# NOTE: tags: description for skins
pgettext("tags", "Allows the player to customize their character by changing the texture or adding accessories.")
# NOTE: tags: title for jam_game_2021
pgettext("tags", "Jam / Game 2021")
# NOTE: tags: description for jam_game_2021
pgettext("tags", "Entries to the 2021 Minetest Game Jam")
# NOTE: tags: title for 128px
pgettext("tags", "128px+")
# NOTE: tags: description for 128px
pgettext("tags", "For 128px or higher texture packs")
# NOTE: tags: title for 16px
pgettext("tags", "16px")
# NOTE: tags: description for 16px
pgettext("tags", "For 16px texture packs")
# NOTE: tags: title for 32px
pgettext("tags", "32px")
# NOTE: tags: description for 32px
pgettext("tags", "For 32px texture packs")
# NOTE: tags: title for 64px
pgettext("tags", "64px")
# NOTE: tags: description for 64px
pgettext("tags", "For 64px texture packs")
# NOTE: tags: title for adventure__rpg
pgettext("tags", "Adventure / RPG")
# NOTE: tags: title for april_fools
pgettext("tags", "Joke")
# NOTE: tags: description for april_fools
pgettext("tags", "For humorous content, meant as a novelty or joke, not to be taken seriously, and that is not meant to be used seriously or long-term.")
# NOTE: tags: title for building
pgettext("tags", "Building")
# NOTE: tags: description for building
pgettext("tags", "Focuses on building, such as adding new materials or nodes")
# NOTE: tags: title for building_mechanics
pgettext("tags", "Building Mechanics and Tools")
# NOTE: tags: description for building_mechanics
pgettext("tags", "Adds game mechanics or tools that change how players build.")
# NOTE: tags: title for chat
pgettext("tags", "Chat / Commands")
# NOTE: tags: description for chat
pgettext("tags", "Focus on player chat/communication or console interaction.")
# NOTE: tags: title for commerce
pgettext("tags", "Commerce / Economy")
# NOTE: tags: description for commerce
pgettext("tags", "Related to economies, money, and trading")
# NOTE: tags: title for complex_installation
pgettext("tags", "Complex installation")
# NOTE: tags: description for complex_installation
pgettext("tags", "Requires futher installation steps, such as installing LuaRocks or editing the trusted mod setting")
# NOTE: tags: title for crafting
pgettext("tags", "Crafting")
# NOTE: tags: description for crafting
pgettext("tags", "Big changes to crafting gameplay")
# NOTE: tags: title for creative
pgettext("tags", "Creative")
# NOTE: tags: description for creative
pgettext("tags", "Written specifically or exclusively for use in creative mode. Adds content only available through a creative inventory, or provides tools that facilitate ingame creation and doesn't add difficulty or scarcity")
# NOTE: tags: title for custom_mapgen
pgettext("tags", "Custom mapgen")
# NOTE: tags: description for custom_mapgen
pgettext("tags", "Contains a completely custom mapgen implemented in Lua, usually requires worlds to be set to the 'singlenode' mapgen.")
# NOTE: tags: title for decorative
pgettext("tags", "Decorative")
# NOTE: tags: description for decorative
pgettext("tags", "Adds nodes with no other purpose than for use in building")
# NOTE: tags: title for developer_tools
pgettext("tags", "Developer Tools")
# NOTE: tags: description for developer_tools
pgettext("tags", "Tools for game and mod developers")
# NOTE: tags: title for education
pgettext("tags", "Education")
# NOTE: tags: description for education
pgettext("tags", "Either has educational value, or is a tool to help teachers ")
# NOTE: tags: title for environment
pgettext("tags", "Environment / Weather")
# NOTE: tags: description for environment
pgettext("tags", "Improves the world, adding weather, ambient sounds, or other environment mechanics")
# NOTE: tags: title for food
pgettext("tags", "Food / Drinks")
# NOTE: tags: title for gui
pgettext("tags", "GUI")
# NOTE: tags: description for gui
pgettext("tags", "For content whose main utility or features are provided within a GUI, on-screen menu, or similar")
# NOTE: tags: title for hud
pgettext("tags", "HUD")
# NOTE: tags: description for hud
pgettext("tags", "For mods that grant the player extra information in the HUD")
# NOTE: tags: title for inventory
pgettext("tags", "Inventory")
# NOTE: tags: description for inventory
pgettext("tags", "Changes the inventory GUI")
# NOTE: tags: title for jam_combat_mod
pgettext("tags", "Jam / Combat 2020")
# NOTE: tags: description for jam_combat_mod
pgettext("tags", "For mods created for the Discord \"Combat\" modding event in 2020")
# NOTE: tags: title for jam_game_2021
pgettext("tags", "Jam / Game 2021")
# NOTE: tags: description for jam_game_2021
pgettext("tags", "Entries to the 2021 Minetest Game Jam")
# NOTE: tags: title for jam_game_2022
pgettext("tags", " Jam / Game 2022")
# NOTE: tags: description for jam_game_2022
pgettext("tags", "Entries to the 2022 Minetest Game Jam ")
# NOTE: content_warnings: title for gore
pgettext("content_warnings", "Gore")
# NOTE: content_warnings: description for gore
pgettext("content_warnings", "Blood, etc")
# NOTE: content_warnings: title for gambling
pgettext("content_warnings", "Gambling")
# NOTE: content_warnings: description for gambling
pgettext("content_warnings", "Games of chance, gambling games, etc")
# NOTE: content_warnings: title for violence
pgettext("content_warnings", "Violence")
# NOTE: content_warnings: description for violence
pgettext("content_warnings", "Non-cartoon violence. May be towards fantasy or human-like characters")
# NOTE: content_warnings: title for horror
pgettext("content_warnings", "Fear / Horror")
# NOTE: content_warnings: description for horror
pgettext("content_warnings", "Shocking and scary content. May scare young children")
# NOTE: content_warnings: title for bad_language
pgettext("content_warnings", "Bad Language")
# NOTE: content_warnings: description for bad_language
pgettext("content_warnings", "Contains swearing")
# NOTE: tags: title for jam_game_2023
pgettext("tags", "Jam / Game 2023")
# NOTE: tags: description for jam_game_2023
pgettext("tags", "Entries to the 2023 Minetest Game Jam ")
# NOTE: tags: title for jam_game_2024
pgettext("tags", "Jam / Game 2024")
# NOTE: tags: description for jam_game_2024
pgettext("tags", "Entries to the 2024 Luanti Game Jam")
# NOTE: tags: title for jam_weekly_2021
pgettext("tags", "Jam / Weekly Challenges 2021")
# NOTE: tags: description for jam_weekly_2021
pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding event in 2021")
# NOTE: tags: title for less_than_px
pgettext("tags", "<16px")
# NOTE: tags: description for less_than_px
pgettext("tags", "For less than 16px texture packs ")
# NOTE: tags: title for library
pgettext("tags", "API / Library")
# NOTE: tags: description for library
pgettext("tags", "Primarily adds an API for other mods to use")
# NOTE: tags: title for magic
pgettext("tags", "Magic / Enchanting")
# NOTE: tags: title for mapgen
pgettext("tags", "Mapgen / Biomes / Decoration")
# NOTE: tags: description for mapgen
pgettext("tags", "New mapgen or changes mapgen")
# NOTE: tags: title for mini-game
pgettext("tags", "Mini-game")
# NOTE: tags: description for mini-game
pgettext("tags", "Adds a mini-game to be played within Luanti")
# NOTE: tags: title for mobs
pgettext("tags", "Mobs / Animals / NPCs")
# NOTE: tags: description for mobs
pgettext("tags", "Adds mobs, animals, and non-player characters")
# NOTE: tags: title for mtg
pgettext("tags", "Minetest Game improved")
# NOTE: tags: description for mtg
pgettext("tags", "Forks of Minetest Game")
# NOTE: tags: title for multiplayer
pgettext("tags", "Multiplayer-focused")
# NOTE: tags: description for multiplayer
pgettext("tags", "Can/should only be used in multiplayer")
# NOTE: tags: title for oneofakind__original
pgettext("tags", "One-of-a-kind / Original")
# NOTE: tags: description for oneofakind__original
pgettext("tags", "For games and such that are of their own kind, distinct and original in nature to others of the same category.")
# NOTE: tags: title for plants_and_farming
pgettext("tags", "Plants and Farming")
# NOTE: tags: description for plants_and_farming
pgettext("tags", "Adds new plants or other farmable resources.")
# NOTE: tags: title for player_effects
pgettext("tags", "Player Effects / Power Ups")
# NOTE: tags: description for player_effects
pgettext("tags", "For content that changes player effects, including physics, for example: speed, jump height or gravity.")
# NOTE: tags: title for puzzle
pgettext("tags", "Puzzle")
# NOTE: tags: description for puzzle
pgettext("tags", "Focus on puzzle solving instead of combat")
# NOTE: tags: title for pve
pgettext("tags", "Player vs Environment (PvE)")
# NOTE: tags: description for pve
pgettext("tags", "For content designed for one or more players that focus on combat against the world, mobs, or NPCs.")
# NOTE: tags: title for pvp
pgettext("tags", "Player vs Player (PvP)")
# NOTE: tags: description for pvp
pgettext("tags", "Designed to be played competitively against other players")
# NOTE: tags: title for seasonal
pgettext("tags", "Seasonal")
# NOTE: tags: description for seasonal
pgettext("tags", "For content generally themed around a certain season or holiday")
# NOTE: tags: title for server_tools
pgettext("tags", "Server Moderation and Tools")
# NOTE: tags: description for server_tools
pgettext("tags", "Helps with server maintenance and moderation")
# NOTE: tags: title for shooter
pgettext("tags", "Shooter")
# NOTE: tags: description for shooter
pgettext("tags", "First person shooters (FPS) and more")
# NOTE: tags: title for simulation
pgettext("tags", "Sims")
# NOTE: tags: description for simulation
pgettext("tags", "Mods and games that aim to simulate real life activity. Similar to SimCity/The Sims/OpenTTD/etc.")
# NOTE: tags: title for singleplayer
pgettext("tags", "Singleplayer-focused")
# NOTE: tags: description for singleplayer
pgettext("tags", "Content that can be played alone")
# NOTE: tags: title for skins
pgettext("tags", "Player customization / Skins")
# NOTE: tags: description for skins
pgettext("tags", "Allows the player to customize their character by changing the texture or adding accessories.")
# NOTE: tags: title for sound_music
pgettext("tags", "Sounds / Music")
# NOTE: tags: description for sound_music
pgettext("tags", "Focuses on or adds new sounds or musical things")
# NOTE: tags: title for sports
pgettext("tags", "Sports")
# NOTE: tags: title for storage
pgettext("tags", "Storage")
# NOTE: tags: description for storage
pgettext("tags", "Adds or improves item storage mechanics")
# NOTE: tags: title for strategy_rts
pgettext("tags", "Strategy / RTS")
# NOTE: tags: description for strategy_rts
pgettext("tags", "Games and mods with a heavy strategy component, whether real-time or turn-based")
# NOTE: tags: title for survival
pgettext("tags", "Survival")
# NOTE: tags: description for survival
pgettext("tags", "Written specifically for survival gameplay with a focus on game-balance, difficulty level, or resources available through crafting, mining, ...")
# NOTE: tags: title for technology
pgettext("tags", "Machines / Electronics")
# NOTE: tags: description for technology
pgettext("tags", "Adds machines useful in automation, tubes, or power.")
# NOTE: tags: title for tools
pgettext("tags", "Tools / Weapons / Armor")
# NOTE: tags: description for tools
pgettext("tags", "Adds or changes tools, weapons, and armor")
# NOTE: tags: title for transport
pgettext("tags", "Transport")
# NOTE: tags: description for transport
pgettext("tags", "Adds or changes transportation methods. Includes teleportation, vehicles, ridable mobs, transport infrastructure and thematic content")
# NOTE: tags: title for world_tools
pgettext("tags", "World Maintenance and Tools")
# NOTE: tags: description for world_tools
pgettext("tags", "Tools to manage the world")
# NOTE: content_warnings: title for alcohol_tobacco
pgettext("content_warnings", "Alcohol / Tobacco")
# NOTE: content_warnings: description for alcohol_tobacco
pgettext("content_warnings", "Contains alcohol and/or tobacco")
# NOTE: content_warnings: title for bad_language
pgettext("content_warnings", "Bad Language")
# NOTE: content_warnings: description for bad_language
pgettext("content_warnings", "Contains swearing")
# NOTE: content_warnings: title for drugs
pgettext("content_warnings", "Drugs")
# NOTE: content_warnings: description for drugs
pgettext("content_warnings", "Contains recreational drugs other than alcohol or tobacco")
# NOTE: content_warnings: title for gambling
pgettext("content_warnings", "Gambling")
# NOTE: content_warnings: description for gambling
pgettext("content_warnings", "Games of chance, gambling games, etc")
# NOTE: content_warnings: title for gore
pgettext("content_warnings", "Gore")
# NOTE: content_warnings: description for gore
pgettext("content_warnings", "Blood, etc")
# NOTE: content_warnings: title for horror
pgettext("content_warnings", "Fear / Horror")
# NOTE: content_warnings: description for horror
pgettext("content_warnings", "Shocking and scary content. May scare young children")
# NOTE: content_warnings: title for violence
pgettext("content_warnings", "Violence")
# NOTE: content_warnings: description for violence
pgettext("content_warnings", "Non-cartoon violence. May be towards fantasy or human-like characters")

View File

@@ -20,16 +20,16 @@ from typing import List
import requests
from celery import group, uuid
from flask import redirect, url_for, flash, current_app
from sqlalchemy import or_, and_
from sqlalchemy import or_, and_, not_, func
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry, ReportAttachment
from app.tasks.emails import send_pending_digests
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
import_languages, check_all_zip_files
from app.tasks.usertasks import import_github_user_ids
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links
from app.tasks.usertasks import import_github_user_ids, do_delete_likely_spammers
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links, update_file_size_bytes
from app.utils import add_notification, get_system_user
actions = {}
@@ -68,9 +68,10 @@ def clean_uploads():
release_urls = get_filenames_from_column(PackageRelease.url)
screenshot_urls = get_filenames_from_column(PackageScreenshot.url)
attachment_urls = get_filenames_from_column(ReportAttachment.url)
pp_urls = get_filenames_from_column(User.profile_pic)
db_urls = release_urls.union(screenshot_urls).union(pp_urls)
db_urls = release_urls.union(screenshot_urls).union(pp_urls).union(attachment_urls)
unreachable = existing_uploads.difference(db_urls)
import sys
@@ -322,6 +323,13 @@ def do_check_all_zip_files():
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("Update file_size_bytes")
def do_update_file_size_bytes():
task_id = uuid()
update_file_size_bytes.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("DANGER: Delete less popular removed packages")
def del_less_popular_removed_packages():
task_id = uuid()
@@ -417,3 +425,10 @@ def delete_empty_threads():
def check_for_broken_links():
for package in Package.query.filter_by(state=PackageState.APPROVED).all():
check_package_for_broken_links.delay(package.id)
@action("DANGER: Delete likely spammers")
def delete_likely_spammers():
task_id = uuid()
do_delete_likely_spammers.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))

View File

@@ -21,9 +21,10 @@ from wtforms import StringField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length, Optional
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
get_int_or_abort
from sqlalchemy import func
from . import bp
from .actions import actions
from app.models import UserRank, Package, db, PackageState, User, AuditSeverity, NotificationType, PackageAlias
from app.models import UserRank, Package, db, PackageState, PackageRelease, PackageScreenshot, User, AuditSeverity, NotificationType, PackageAlias
from ...querybuilder import QueryBuilder
@@ -182,6 +183,17 @@ def transfer():
return render_template("admin/transfer.html", form=form)
def sum_file_sizes(clazz):
ret = {}
for entry in (db.session
.query(clazz.package_id, func.sum(clazz.file_size_bytes))
.select_from(clazz)
.group_by(clazz.package_id)
.all()):
ret[entry[0]] = entry[1]
return ret
@bp.route("/admin/storage/")
@rank_required(UserRank.EDITOR)
def storage():
@@ -192,15 +204,20 @@ def storage():
show_all = len(packages) < 100
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50)
package_size_releases = sum_file_sizes(PackageRelease)
package_size_screenshots = sum_file_sizes(PackageScreenshot)
data = []
for package in packages:
size_releases = sum([x.file_size_bytes for x in package.releases])
size_screenshots = sum([x.file_size_bytes for x in package.screenshots])
size_releases = package_size_releases.get(package.id, 0)
size_screenshots = package_size_screenshots.get(package.id, 0)
size_total = size_releases + size_screenshots
if size_total < min_size * 1024 * 1024:
continue
latest_release = package.releases.first()
size_latest = latest_release.file_size_bytes if latest_release else 0
size_total = size_releases + size_screenshots
if size_total > min_size*1024*1024:
data.append([package, size_total, size_releases, size_screenshots, size_latest])
data.append([package, size_total, size_releases, size_screenshots, size_latest])
data.sort(key=lambda x: x[1], reverse=True)
return render_template("admin/storage.html", data=data)

View File

@@ -15,7 +15,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template, request, abort
from flask_babel import lazy_gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import Optional, Length
from app.models import db, AuditLogEntry, UserRank, User, Permission
from app.utils import rank_required, get_int_or_abort
@@ -23,26 +27,40 @@ from app.utils import rank_required, get_int_or_abort
from . import bp
class AuditForm(FlaskForm):
username = StringField(lazy_gettext("Username"), [Optional(), Length(0, 25)])
q = StringField(lazy_gettext("Query"), [Optional(), Length(0, 300)])
url = StringField(lazy_gettext("URL"), [Optional(), Length(0, 300)])
submit = SubmitField(lazy_gettext("Search"), name=None)
@bp.route("/admin/audit/")
@rank_required(UserRank.MODERATOR)
@rank_required(UserRank.APPROVER)
def audit():
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 100))
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
if "username" in request.args:
user = User.query.filter_by(username=request.args.get("username")).first()
if not user:
abort(404)
form = AuditForm(request.args)
username = form.username.data
q = form.q.data
url = form.url.data
if username:
user = User.query.filter_by(username=username).first_or_404()
query = query.filter_by(causer=user)
if "q" in request.args:
q = request.args["q"]
if q:
query = query.filter(AuditLogEntry.title.ilike(f"%{q}%"))
if url:
query = query.filter(AuditLogEntry.url.ilike(f"%{url}%"))
if not current_user.rank.at_least(UserRank.MODERATOR):
query = query.filter(AuditLogEntry.package)
pagination = query.paginate(page=page, per_page=num)
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
return render_template("admin/audit.html", log=pagination.items, pagination=pagination, form=form)
@bp.route("/admin/audit/<int:id_>/")

View File

@@ -23,14 +23,14 @@ from wtforms.validators import InputRequired, Length
from app.utils import rank_required, add_audit_log
from . import bp
from app.models import UserRank, MinetestRelease, db, AuditSeverity
from app.models import UserRank, LuantiRelease, db, AuditSeverity
@bp.route("/versions/")
@rank_required(UserRank.MODERATOR)
def version_list():
return render_template("admin/versions/list.html",
versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
versions=LuantiRelease.query.order_by(db.asc(LuantiRelease.id)).all())
class VersionForm(FlaskForm):
@@ -45,14 +45,14 @@ class VersionForm(FlaskForm):
def create_edit_version(name=None):
version = None
if name is not None:
version = MinetestRelease.query.filter_by(name=name).first()
version = LuantiRelease.query.filter_by(name=name).first()
if version is None:
abort(404)
form = VersionForm(formdata=request.form, obj=version)
if form.validate_on_submit():
if version is None:
version = MinetestRelease(form.name.data)
version = LuantiRelease(form.name.data)
db.session.add(version)
flash("Created version " + form.name.data, "success")

View File

@@ -29,12 +29,12 @@ from app import csrf
from app.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats
from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
LuantiRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
PackageAlias, Language
from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date, cached, \
cors_allowed
from app.utils.minetest_hypertext import html_to_minetest, package_info_as_hypertext, package_reviews_as_hypertext
from app.utils.luanti_hypertext import html_to_luanti, package_info_as_hypertext, package_reviews_as_hypertext
from . import bp
from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
@@ -102,7 +102,7 @@ def package_view_client(package: Package):
protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
version = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
version = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
else:
version = None
@@ -113,9 +113,10 @@ def package_view_client(package: Package):
formspec_version = get_int_or_abort(request.args["formspec_version"])
include_images = is_yes(request.args.get("include_images", "true"))
html = render_markdown(data["long_description"])
page_url = package.get_url("packages.view", absolute=True)
data["long_description"] = html_to_minetest(html, page_url, formspec_version, include_images)
if data["long_description"] is not None:
html = render_markdown(data["long_description"])
data["long_description"] = html_to_luanti(html, page_url, formspec_version, include_images)
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
@@ -150,9 +151,9 @@ def package_view_client_reviews(package: Package):
def package_hypertext(package):
formspec_version = get_int_or_abort(request.args["formspec_version"])
include_images = is_yes(request.args.get("include_images", "true"))
html = render_markdown(package.desc)
html = render_markdown(package.desc if package.desc else "")
page_url = package.get_url("packages.view", absolute=True)
return jsonify(html_to_minetest(html, page_url, formspec_version, include_images))
return jsonify(html_to_luanti(html, page_url, formspec_version, include_images))
@bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
@@ -569,14 +570,14 @@ def package_scores():
@cors_allowed
@cached(60*60)
def tags():
return jsonify([tag.as_dict() for tag in Tag.query.all() ])
return jsonify([tag.as_dict() for tag in Tag.query.order_by(db.asc(Tag.name)).all()])
@bp.route("/api/content_warnings/")
@cors_allowed
@cached(60*60)
def content_warnings():
return jsonify([warning.as_dict() for warning in ContentWarning.query.all() ])
return jsonify([warning.as_dict() for warning in ContentWarning.query.order_by(db.asc(ContentWarning.name)).all() ])
@bp.route("/api/licenses/")
@@ -629,38 +630,20 @@ def homepage():
})
@bp.route("/api/welcome/v1/")
@cors_allowed
def welcome_v1():
featured = Package.query \
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
Package.collections.any(
and_(Collection.name == "featured", Collection.author.has(username="ContentDB")))) \
.order_by(func.random()) \
.limit(5).all()
def map_packages(packages: List[Package]):
return [pkg.as_short_dict(current_app.config["BASE_URL"]) for pkg in packages]
return jsonify({
"featured": map_packages(featured),
})
@bp.route("/api/minetest_versions/")
@cors_allowed
def versions():
protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
rel = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
if rel is None:
error(404, "No releases found")
return jsonify(rel.as_dict())
return jsonify([rel.as_dict() \
for rel in MinetestRelease.query.all() if rel.get_actual() is not None])
for rel in LuantiRelease.query.all() if rel.get_actual() is not None])
@bp.route("/api/languages/")
@@ -852,7 +835,7 @@ def hypertext():
if request.content_type == "text/markdown":
html = render_markdown(html)
return jsonify(html_to_minetest(html, "", formspec_version, include_images))
return jsonify(html_to_luanti(html, "", formspec_version, include_images))
@bp.route("/api/collections/")
@@ -903,9 +886,9 @@ def collection_view(token, author, name):
@cached(300)
def updates():
protocol_version = get_int_or_abort(request.args.get("protocol_version"))
minetest_version = request.args.get("engine_version")
if protocol_version or minetest_version:
version = MinetestRelease.get(minetest_version, protocol_version)
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
version = LuantiRelease.get(engine_version, protocol_version)
else:
version = None

View File

@@ -20,7 +20,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
from app.logic.packages import do_edit_package
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
from app.models import APIToken, Package, LuantiRelease, PackageScreenshot
def error(code: int, msg: str):
@@ -39,7 +39,7 @@ def guard(f):
def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API"):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
@@ -55,7 +55,7 @@ def api_create_vcs_release(token: APIToken, package: Package, name: str, title:
def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API", commit_hash: str = None):
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API", commit_hash: str = None):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
@@ -112,9 +112,9 @@ def api_edit_package(token: APIToken, package: Package, data: dict, reason: str
reason += ", token=" + token.name
package = guard(do_edit_package)(token.owner, package, False, False, data, reason)
was_modified = guard(do_edit_package)(token.owner, package, False, False, data, reason)
return jsonify({
"success": True,
"package": package.as_dict(current_app.config["BASE_URL"])
"package": package.as_dict(current_app.config["BASE_URL"]),
"was_modified": was_modified,
})

View File

@@ -17,7 +17,7 @@
import re
import typing
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for, jsonify
from flask_babel import lazy_gettext, gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
@@ -25,7 +25,7 @@ from wtforms import StringField, BooleanField, SubmitField, FieldList, HiddenFie
from wtforms.validators import InputRequired, Length, Optional, Regexp
from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity
from app.utils import nonempty_or_none, normalize_line_endings
from app.utils import nonempty_or_none, normalize_line_endings, should_return_json
from app.utils.models import is_package_page, add_audit_log, create_session
bp = Blueprint("collections", __name__)
@@ -70,7 +70,10 @@ def view(author, name):
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
items = [x for x in items if x.package.check_perm(current_user, Permission.VIEW_PACKAGE)]
return render_template("collections/view.html", collection=collection, items=items)
if should_return_json():
return jsonify([ item.package.as_key_dict() for item in items ])
else:
return render_template("collections/view.html", collection=collection, items=items)
class CollectionForm(FlaskForm):

View File

@@ -29,10 +29,10 @@ def _make_feed(title: str, feed_url: str, items: list):
return {
"version": "https://jsonfeed.org/version/1",
"title": title,
"description": gettext("Welcome to the best place to find Minetest mods, games, and texture packs"),
"home_page_url": "https://content.minetest.net/",
"description": gettext("Welcome to the best place to find Luanti mods, games, and texture packs"),
"home_page_url": "https://content.luanti.org/",
"feed_url": feed_url,
"icon": "https://content.minetest.net/favicon-128.png",
"icon": "https://content.luanti.org/favicon-128.png",
"expired": False,
"items": items,
}

View File

@@ -28,7 +28,7 @@ from sqlalchemy.sql.expression import func
PKGS_PER_ROW = 4
# GAMEJAM_BANNER = "https://jam.minetest.net/img/banner.png"
# GAMEJAM_BANNER = "https://jam.luanti.org/img/banner.png"
#
# class GameJam:
# cover_image = type("", (), dict(url=GAMEJAM_BANNER))()
@@ -40,7 +40,7 @@ PKGS_PER_ROW = 4
# def get_url(self, _name):
# return "/gamejam/"
#
# title = "Minetest Game Jam 2023: \"Unexpected\""
# title = "Luanti Game Jam 2023: \"Unexpected\""
# author = None
#
# short_desc = "The game jam has finished! It's now up to the community to play and rate the games."
@@ -51,7 +51,7 @@ PKGS_PER_ROW = 4
@bp.route("/gamejam/")
def gamejam():
return redirect("https://jam.minetest.net/")
return redirect("https://jam.luanti.org/")
@bp.route("/")

View File

@@ -194,6 +194,10 @@ def create_edit_client(username, id_=None):
if form.validate_on_submit():
if is_new:
if OAuthClient.query.filter(OAuthClient.title.ilike(form.title.data.strip())).count() > 0:
flash(gettext("An OAuth client with that title already exists. Please choose a new title."), "danger")
return render_template("oauth/create_edit.html", user=user, form=form, client=client)
client = OAuthClient()
db.session.add(client)
client.owner = user
@@ -201,6 +205,7 @@ def create_edit_client(username, id_=None):
client.secret = random_string(32)
client.approved = current_user.rank.at_least(UserRank.EDITOR)
form.populate_obj(client)
verb = "Created" if is_new else "Edited"

View File

@@ -23,7 +23,7 @@ from wtforms.validators import Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
from . import bp
from ...models import PackageType, Tag, db, ContentWarning, License, Language, MinetestRelease, Package, PackageState
from ...models import PackageType, Tag, db, ContentWarning, License, Language, LuantiRelease, Package, PackageState
def make_label(obj: Tag | ContentWarning):
@@ -74,8 +74,8 @@ class AdvancedSearchForm(FlaskForm):
allow_blank=True, blank_value="",
get_pk=lambda a: a.id, get_label=lambda a: a.title)
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
engine_version = QuerySelectField(lazy_gettext("Minetest Version"),
query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)),
engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
query_factory=lambda: LuantiRelease.query.order_by(db.asc(LuantiRelease.id)),
allow_blank=True, blank_value="",
get_pk=lambda a: a.value, get_label=lambda a: a.name)
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[

View File

@@ -233,7 +233,7 @@ class PackageForm(FlaskForm):
name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_) only"))])
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
dev_state = SelectField(lazy_gettext("Maintenance State"), [InputRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
dev_state = SelectField(lazy_gettext("Maintenance State"), [DataRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
tags = QuerySelectMultipleField(lazy_gettext('Tags'), query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=make_label)
content_warnings = QuerySelectMultipleField(lazy_gettext('Content Warnings'), query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=make_label)
@@ -266,6 +266,7 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
flash(
gettext("Package already exists, but is removed. Please contact ContentDB staff to restore the package"),
"danger")
return redirect(url_for("report.report", url=package.get_url("packages.view")))
else:
flash(markupsafe.Markup(
f"<a class='btn btn-sm btn-danger float-end' href='{package.get_url('packages.view')}'>View</a>" +
@@ -305,10 +306,6 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
"translation_url": form.translation_url.data,
})
if wasNew:
msg = f"Created package {author.username}/{form.name.data}"
add_audit_log(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package)
if wasNew and package.repo is not None:
import_repo_screenshot.delay(package.id)
@@ -321,13 +318,14 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
return redirect(next_url)
except LogicError as e:
flash(e.message, "danger")
db.session.rollback()
@bp.route("/packages/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
@login_required
def create_edit(author=None, name=None):
if current_user.email is None:
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
return redirect(url_for("users.email_notifications"))
@@ -458,6 +456,7 @@ def move_to_state(package):
@is_package_page
def translation(package):
return render_template("packages/translation.html", package=package,
has_content_translations=any([x.title or x.short_desc for x in package.translations.all()]),
tabs=get_package_tabs(current_user, package), current_tab="translation")
@@ -570,7 +569,7 @@ def edit_maintainers(package):
for user in users:
if not user in package.maintainers:
if thread:
if thread and user not in thread.watchers:
thread.watchers.append(user)
add_notification(user, current_user, NotificationType.MAINTAINER,
"Added you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package)

View File

@@ -25,7 +25,7 @@ from wtforms.validators import InputRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, MinetestRelease, \
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, LuantiRelease, \
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
from app.rediscache import has_key, set_temp_key, make_download_key
from app.tasks.importtasks import check_update_config
@@ -42,11 +42,11 @@ def list_releases(package):
def get_mt_releases(is_max):
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
query = LuantiRelease.query.order_by(db.asc(LuantiRelease.id))
if is_max:
query = query.limit(query.count() - 1)
else:
query = query.filter(MinetestRelease.name != "0.4.17")
query = query.filter(LuantiRelease.name != "0.4.17")
return query
@@ -59,9 +59,9 @@ class CreatePackageReleaseForm(FlaskForm):
upload_mode = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload")
vcs_label = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None)
file_upload = FileField(lazy_gettext("File Upload"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save"))
@@ -74,9 +74,9 @@ class EditPackageReleaseForm(FlaskForm):
url = StringField(lazy_gettext("URL"), [Optional()])
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
approved = BooleanField(lazy_gettext("Is Approved"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save"))
@@ -85,7 +85,7 @@ class EditPackageReleaseForm(FlaskForm):
@login_required
@is_package_page
def create_release(package):
if current_user.email is None:
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
return redirect(url_for("users.email_notifications"))
@@ -127,9 +127,10 @@ def download_release(package, id):
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
if ip is not None and not is_user_bot():
is_minetest = (request.headers.get("User-Agent") or "").startswith("Minetest")
user_agent = request.headers.get("User-Agent") or ""
is_luanti = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
reason = request.args.get("reason")
PackageDailyStats.update(package, is_minetest, reason)
PackageDailyStats.update(package, is_luanti, reason)
key = make_download_key(ip, release.package)
if not has_key(key):
@@ -214,10 +215,10 @@ def edit_release(package, id):
class BulkReleaseForm(FlaskForm):
set_min = BooleanField(lazy_gettext("Set Min"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
set_max = BooleanField(lazy_gettext("Set Max"))
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
submit = SubmitField(lazy_gettext("Update"))

View File

@@ -19,8 +19,9 @@ from flask import render_template, request, redirect, flash, url_for, abort
from flask_babel import lazy_gettext, gettext
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileRequired
from wtforms import StringField, SubmitField, BooleanField, FileField
from wtforms.validators import InputRequired, Length, DataRequired, Optional
from wtforms.validators import Length, DataRequired, Optional
from wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.LogicError import LogicError
@@ -32,7 +33,7 @@ from app.utils import is_package_page
class CreateScreenshotForm(FlaskForm):
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
file_upload = FileField(lazy_gettext("File Upload"), [InputRequired()])
file_upload = FileField(lazy_gettext("File Upload"), [FileRequired()])
submit = SubmitField(lazy_gettext("Save"))

View File

@@ -14,24 +14,30 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Blueprint, request, render_template, url_for, abort
from flask import Blueprint, request, render_template, url_for, abort, flash
from flask_babel import lazy_gettext
from flask_login import current_user
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from wtforms import TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length
from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
from wtforms.validators import InputRequired, Length, Optional, DataRequired
from app.models import User, UserRank
from app.tasks.emails import send_user_email
from app.logic.uploads import upload_file
from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_no, abs_url_samesite, normalize_line_endings
from app.utils import (is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for,
random_string, add_replies)
bp = Blueprint("report", __name__)
class ReportForm(FlaskForm):
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)], filters=[normalize_line_endings])
category = SelectField(lazy_gettext("Category"), [DataRequired()], choices=ReportCategory.choices(with_none=True), coerce=ReportCategory.coerce)
url = URLField(lazy_gettext("URL"), [Optional()])
title = StringField(lazy_gettext("Subject / Title"), [InputRequired(), Length(10, 300)])
message = TextAreaField(lazy_gettext("Message"), [Optional(), Length(0, 10000)], filters=[normalize_line_endings])
file_upload = FileField(lazy_gettext("Image Upload"), [Optional()])
submit = SubmitField(lazy_gettext("Report"))
@@ -46,22 +52,131 @@ def report():
url = abs_url_samesite(url)
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
form = ReportForm() if current_user.is_authenticated else None
if form and request.method == "GET":
try:
form.category.data = ReportCategory.coerce(request.args.get("category"))
except KeyError:
pass
form.url.data = url
form.title.data = request.args.get("title", "")
if form and form.validate_on_submit():
report = Report()
report.id = random_string(8)
report.user = current_user if current_user.is_authenticated else None
form.populate_obj(report)
if current_user.is_authenticated:
user_info = f"{current_user.username}"
thread = Thread()
thread.title = f"Report: {form.title.data}"
thread.author = current_user
thread.private = True
thread.watchers.extend(User.query.filter(User.rank >= UserRank.MODERATOR).all())
db.session.add(thread)
db.session.flush()
report.thread = thread
add_replies(thread, current_user, f"**{report.category.title} report created**\n\n{form.message.data}")
else:
user_info = request.headers.get("X-Forwarded-For") or request.remote_addr
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
report.message = ip_addr + "\n\n" + report.message
text = f"{url}\n\n{form.message.data}"
db.session.add(report)
db.session.flush()
task = None
for admin in User.query.filter_by(rank=UserRank.ADMIN).all():
task = send_user_email.delay(admin.email, admin.locale or "en",
f"User report from {user_info}", text)
if form.file_upload.data:
atmt = ReportAttachment()
report.attachments.add(atmt)
uploaded_url, _ = upload_file(form.file_upload.data, "image", lazy_gettext("a PNG, JPEG, or WebP image file"))
atmt.url = uploaded_url
db.session.add(atmt)
post_discord_webhook.delay(None if is_anon else current_user.username, f"**New Report**\n{url}\n\n{form.message.data}", True)
if current_user.is_authenticated:
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
url_for("report.view", rid=report.id))
return redirect(url_for("tasks.check", id=task.id, r=url_for("homepage.home")))
db.session.commit()
return render_template("report/index.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
abs_url = abs_url_for("report.view", rid=report.id)
msg = f"**New Report**\nReport on `{report.url}`\n\n{report.title}\n\nView: {abs_url}"
post_discord_webhook.delay(None if is_anon else current_user.username, msg, True)
return redirect(url_for("report.report_received", rid=report.id))
return render_template("report/report.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
@bp.route("/report/received/")
def report_received():
rid = request.args.get("rid")
report = Report.query.get_or_404(rid)
return render_template("report/report_received.html", report=report)
@bp.route("/admin/reports/")
@rank_required(UserRank.EDITOR)
def list_all():
reports = Report.query.order_by(db.asc(Report.is_resolved), db.asc(Report.created_at)).all()
return render_template("report/list.html", reports=reports)
@bp.route("/admin/reports/<rid>/", methods=["GET", "POST"])
def view(rid: str):
report = Report.query.get_or_404(rid)
if not report.check_perm(current_user, Permission.SEE_REPORT):
abort(404)
if request.method == "POST":
if report.is_resolved:
if "reopen" in request.form:
report.is_resolved = False
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Reopened report \"{report.title}\"", url)
if report.thread:
add_replies(report.thread, current_user, f"Reopened report", is_status_update=True)
db.session.commit()
else:
if "completed" in request.form:
outcome = "as completed"
elif "removed" in request.form:
outcome = "as content removed"
elif "invalid" in request.form:
outcome = "without action"
if report.thread:
flash("Make sure to comment why the report is invalid in the thread", "warning")
else:
abort(400)
report.is_resolved = True
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Report closed {outcome} \"{report.title}\"", url)
if report.thread:
add_replies(report.thread, current_user, f"Closed report {outcome}", is_status_update=True)
db.session.commit()
return render_template("report/view.html", report=report)
@bp.route("/admin/reports/<rid>/edit/", methods=["GET", "POST"])
def edit(rid: str):
report = Report.query.get_or_404(rid)
if not report.check_perm(current_user, Permission.SEE_REPORT):
abort(404)
form = ReportForm(request.form, obj=report)
form.submit.label.text = lazy_gettext("Save")
if form.validate_on_submit():
form.populate_obj(report)
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited report \"{report.title}\"", url)
db.session.commit()
return redirect(url_for("report.view", rid=report.id))
return render_template("report/edit.html", report=report, form=form)

View File

@@ -29,7 +29,7 @@ from app.models import Package, db, User, Permission, Thread, UserRank, AuditSev
from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains, \
normalize_line_endings
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, BooleanField
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length
from app.utils import get_int_or_abort
@@ -254,6 +254,9 @@ def view(id):
if mentioned is None:
continue
if not thread.check_perm(mentioned, Permission.SEE_THREAD):
continue
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
msg, thread.get_view_url(), thread.package)
@@ -281,7 +284,6 @@ def view(id):
class ThreadForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)])
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
private = BooleanField(lazy_gettext("Private"))
btn_submit = SubmitField(lazy_gettext("Open Thread"))
@@ -296,14 +298,11 @@ def new():
if package is None:
abort(404)
def_is_private = request.args.get("private") or False
if package is None and not current_user.rank.at_least(UserRank.APPROVER):
abort(404)
is_review_thread = package and not package.approved
allow_private_change = not is_review_thread
if is_review_thread:
def_is_private = True
is_private_thread = is_review_thread
# Check that user can make the thread
if package and not package.check_perm(current_user, Permission.CREATE_THREAD):
@@ -326,7 +325,6 @@ def new():
# Set default values
elif request.method == "GET":
form.private.data = def_is_private
form.title.data = request.args.get("title") or ""
# Validate and submit
@@ -337,7 +335,7 @@ def new():
thread = Thread()
thread.author = current_user
thread.title = form.title.data
thread.private = form.private.data if allow_private_change else def_is_private
thread.private = is_private_thread
thread.package = package
db.session.add(thread)
@@ -367,7 +365,8 @@ def new():
add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.get_view_url(), thread.package)
thread.watchers.append(mentioned)
if mentioned not in thread.watchers:
thread.watchers.append(mentioned)
notif_msg = "New thread '{}'".format(thread.title)
if package is not None:
@@ -384,7 +383,7 @@ def new():
return redirect(thread.get_view_url())
return render_template("threads/new.html", form=form, allow_private_change=allow_private_change, package=package)
return render_template("threads/new.html", form=form, package=package)
@bp.route("/users/<username>/comments/")

View File

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

View File

@@ -20,7 +20,7 @@ from flask_login import current_user, login_required
from sqlalchemy import or_, and_
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, MinetestRelease
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, LuantiRelease, Report
from app.querybuilder import QueryBuilder
from app.utils import get_int_or_abort, is_yes, rank_required
from . import bp
@@ -83,11 +83,13 @@ def view_editor():
.order_by(db.desc(AuditLogEntry.created_at)) \
.limit(20).all()
reports = Report.query.filter_by(is_resolved=False).order_by(db.asc(Report.created_at)).all() if current_user.rank.at_least(UserRank.EDITOR) else None
return render_template("todo/editor.html", current_tab="editor",
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
can_approve_new=can_approve_new, can_approve_rel=can_approve_rel, can_approve_scn=can_approve_scn,
license_needed=license_needed, total_packages=total_packages, total_to_tag=total_to_tag,
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log)
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log, reports=reports)
@bp.route("/todo/tags/")
@@ -170,7 +172,7 @@ def screenshots():
def mtver_support():
is_mtm_only = is_yes(request.args.get("mtm"))
current_stable = MinetestRelease.query.filter(~MinetestRelease.name.like("%-dev")).order_by(db.desc(MinetestRelease.id)).first()
current_stable = LuantiRelease.query.filter(~LuantiRelease.name.like("%-dev")).order_by(db.desc(LuantiRelease.id)).first()
query = db.session.query(Package) \
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \

View File

@@ -104,7 +104,7 @@ class RegisterForm(FlaskForm):
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
agree = BooleanField(lazy_gettext("I agree"), [DataRequired()])
first_name = StringField("First name", [])
submit = SubmitField(lazy_gettext("Register"))
@@ -118,6 +118,8 @@ def handle_register(form):
return user
elif user is None:
return
elif form.first_name.data != "":
abort(500)
user.password = make_flask_login_password(form.password.data)

View File

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

View File

@@ -20,6 +20,7 @@ from app.models import Package, APIToken, Permission, PackageState
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
repo_url = repo_url.replace("https://", "").replace("http://", "").lower()
if token.package:
packages = [token.package]
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):

View File

@@ -28,7 +28,6 @@ from app.blueprints.api.support import error, api_create_vcs_release
from app.logic.users import create_user
from app.models import db, User, APIToken, AuditSeverity
from app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
from . import bp
from .common import get_packages_for_vcs_and_token
@@ -98,6 +97,7 @@ def github_callback(oauth_token):
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
return redirect(url_for("users.login"))
user_by_github.github_username = github_username
add_audit_log(AuditSeverity.USER, user_by_github, "Logged in using GitHub OAuth",
url_for("users.profile", username=user_by_github.username))
db.session.commit()

View File

@@ -38,7 +38,7 @@ def webhook_impl():
if token is None:
return error(403, "Invalid authentication")
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"].replace("https://", "").replace("http://", ""))
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"])
for package in packages:
#
# Check event

View File

@@ -14,27 +14,27 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from celery import uuid
from flask import Blueprint, render_template, redirect, request, abort, url_for
from flask_babel import lazy_gettext
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import InputRequired, Length
from wtforms import StringField, BooleanField, SubmitField, SelectMultipleField
from wtforms.validators import InputRequired, Length, Optional
from app.tasks import celery
from app.utils import rank_required
bp = Blueprint("zipgrep", __name__)
from app.models import UserRank, Package
from app.models import UserRank, Package, PackageType
from app.tasks.zipgrep import search_in_releases
class SearchForm(FlaskForm):
query = StringField(lazy_gettext("Text to find (regex)"), [InputRequired(), Length(1, 100)])
file_filter = StringField(lazy_gettext("File filter"), [InputRequired(), Length(1, 100)], default="*.lua")
remember_me = BooleanField(lazy_gettext("Remember me"), default=True)
type = SelectMultipleField(lazy_gettext("Type"), [Optional()],
choices=PackageType.choices(), coerce=PackageType.coerce)
submit = SubmitField(lazy_gettext("Search"))
@@ -44,7 +44,7 @@ def zipgrep_search():
form = SearchForm(request.form)
if form.validate_on_submit():
task_id = uuid()
search_in_releases.apply_async((form.query.data, form.file_filter.data), task_id=task_id)
search_in_releases.apply_async((form.query.data, form.file_filter.data, [x.name for x in form.type.data]), task_id=task_id)
result_url = url_for("zipgrep.view_results", id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=result_url))

View File

@@ -16,7 +16,7 @@
import datetime
from .models import User, UserRank, MinetestRelease, Tag, License, Notification, NotificationType, Package, \
from .models import User, UserRank, LuantiRelease, Tag, License, Notification, NotificationType, Package, \
PackageState, PackageType, PackageRelease, MetaPackage, Dependency
from .utils import make_flask_login_password
@@ -35,12 +35,12 @@ def populate(session):
system_user.rank = UserRank.BOT
session.add(system_user)
session.add(MinetestRelease("None", 0))
session.add(MinetestRelease("0.4.16/17", 32))
session.add(MinetestRelease("5.0", 37))
session.add(MinetestRelease("5.1", 38))
session.add(MinetestRelease("5.2", 39))
session.add(MinetestRelease("5.3", 39))
session.add(LuantiRelease("None", 0))
session.add(LuantiRelease("0.4.16/17", 32))
session.add(LuantiRelease("5.0", 37))
session.add(LuantiRelease("5.1", 38))
session.add(LuantiRelease("5.2", 39))
session.add(LuantiRelease("5.3", 39))
tags = {}
for tag in ["Inventory", "Mapgen", "Building",
@@ -69,8 +69,8 @@ def populate_test_data(session):
licenses = { x.name : x for x in License.query.all() }
tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
v4 = MinetestRelease.query.filter_by(protocol=32).first()
v51 = MinetestRelease.query.filter_by(protocol=38).first()
v4 = LuantiRelease.query.filter_by(protocol=32).first()
v51 = LuantiRelease.query.filter_by(protocol=38).first()
ez = User("Shara")
ez.github_username = "Ezhh"

View File

@@ -10,8 +10,8 @@ as it was submitted as university coursework. To learn about the history and dev
ContentDB is open source software, licensed under AGPLv3.0.
<a href="https://github.com/minetest/contentdb/" class="btn btn-primary me-1">Source code</a>
<a href="https://github.com/minetest/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
<a href="https://github.com/luanti-org/contentdb/" class="btn btn-primary me-1">Source code</a>
<a href="https://github.com/luanti-org/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
{% if monitoring_url -%}
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a>
@@ -25,13 +25,13 @@ poor user experience, especially for first-time users.
ContentDB isn't just about supporting the in-game content downloader; it's common for technical users to find
and review packages using the ContentDB website, but install using Git rather than the in-game installer.
**ContentDB's purpose is to be a well-formatted source of information about mods, games,
and texture packs for Minetest**.
and texture packs for Luanti**.
## How do I learn how to make mods and games for Minetest?
## How do I learn how to make mods and games for Luanti?
You should read
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest.
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
<h2 id="donate">How can I support / donate to ContentDB?</h2>
@@ -45,5 +45,5 @@ For more information about the cost of ContentDB and what rubenwardy does, see h
## Sponsorships
Minetest and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
Luanti and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
This provides us with improved error logging and performance insights.

View File

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

View File

@@ -3,7 +3,7 @@ title: API
## Resources
* [How the Minetest client uses the API](https://github.com/minetest/contentdb/blob/master/docs/minetest_client.md)
* [How the Luanti client uses the API](https://github.com/luanti-org/contentdb/blob/master/docs/luanti_client.md)
## Responses and Error Handling
@@ -54,7 +54,7 @@ The response will be a dictionary with the following keys:
Not all endpoints require authentication, but it is done using Bearer tokens:
```bash
curl https://content.minetest.net/api/whoami/ \
curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN"
```
@@ -67,8 +67,8 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* DELETE `/api/delete-token/`: Deletes the currently used token.
```bash
# Logout
curl -X DELETE https://content.minetest.net/api/delete-token/ \
# Logout
curl -X DELETE https://content.luanti.org/api/delete-token/ \
-H "Authorization: Bearer YOURTOKEN"
```
@@ -78,9 +78,12 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* GET `/api/packages/` (List)
* See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/` (Read)
* Redirects a JSON object with the keys documented by the PUT endpoint, below.
* Plus:
* `forum_url`: String or null.
* PUT `/api/packages/<author>/<name>/` (Update)
* Requires authentication.
* JSON dictionary with any of these keys (all are optional, null to delete Nullables):
* JSON object with any of these keys (all are optional, null to delete Nullables):
* `type`: One of `GAME`, `MOD`, `TXP`.
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
@@ -99,9 +102,13 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* `video_url`: URL to a video.
* `donate_url`: URL to a donation page.
* `translation_url`: URL to send users interested in translating your package.
* `game_support`: Array of game support information objects. Not currently documented, as subject to change.
* `game_support`: Array of game support information objects. Not currently documented,
* Returns a JSON object with:
* `success`
* `package`: updated package
* `was_modified`: bool, whether anything changed
* GET `/api/packages/<username>/<name>/for-client/`
* Similar to the read endpoint, but optimised for the Minetest client
* Similar to the read endpoint, but optimised for the Luanti client
* `long_description` is given as a hypertext object, see `/hypertext/` below.
* `info_hypertext` is the info sidebar as a hypertext object.
* Query arguments
@@ -109,17 +116,31 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
* `protocol_version`: Optional, used to get the correct release.
* `engine_version`: Optional, used to get the correct release. Ex: `5.3.0`.
* GET `/api/packages/<username>/<name>/for-client/reviews/`
* Returns hypertext representing the package's reviews
* Query arguments
* `formspec_version`: Required. See /hypertext/ below.
* Returns JSON dictionary with following keys:
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
* `body`: markup for long description.
* `links`: dictionary of anchor name to link URL.
* `images`: dictionary of img name to image URL.
* `image_tooltips`: dictionary of img name to tooltip text.
* The hypertext body contains some placeholders that should be replaced client-side:
* `<thumbsup>` with a thumbs up icon.
* `<neutral>` with a thumbs up icon.
* `<thumbsdown>` with a thumbs up icon.
* GET `/api/packages/<author>/<name>/hypertext/`
* Converts the long description to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
* Converts the long description to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* Query arguments:
* `formspec_version`: Required, maximum supported formspec version.
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
* Returns JSON dictionary with following key:
* Returns JSON dictionary with following keys:
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
* `body`: markup for long description.
* `links`: dictionary of anchor name to link URL.
* `images`: dictionary of img name to image URL
* `images`: dictionary of img name to image URL.
* `image_tooltips`: dictionary of img name to tooltip text.
* GET `/api/packages/<username>/<name>/dependencies/`
* Returns dependencies, with suggested candidates
@@ -163,20 +184,20 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
You can download a package by building one of the two URLs:
```
https://content.minetest.net/packages/${author}/${name}/download/`
https://content.minetest.net/packages/${author}/${name}/releases/${release}/download/`
https://content.luanti.org/packages/${author}/${name}/download/`
https://content.luanti.org/packages/${author}/${name}/releases/${release}/download/`
```
Examples:
```bash
# Edit package
curl -X PUT https://content.minetest.net/api/packages/username/name/ \
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }'
# Remove website URL
curl -X PUT https://content.minetest.net/api/packages/username/name/ \
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "website": null }'
```
@@ -198,8 +219,8 @@ Filter query parameters:
* `license`: Filter by [license name](#licenses). Multiple licenses are OR-ed together, ie: `&license=MIT&license=LGPL-2.1-only`
* `game`: Filter by [Game Support](/help/game_support/), ex: `Warr1024/nodecore`. (experimental, doesn't show items that support every game currently).
* `lang`: Filter by translation support, eg: `en`/`de`/`ja`/`zh_TW`.
* `protocol_version`: Only show packages supported by this Minetest protocol version.
* `engine_version`: Only show packages supported by this Minetest engine version, eg: `5.3.0`.
* `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
Sorting query parameters:
@@ -212,7 +233,7 @@ Format query parameters:
* `limit`: Return at most `limit` packages.
* `fmt`: How the response is formatted.
* `keys`: author/name only.
* `short`: stuff needed for the Minetest client.
* `short`: stuff needed for the Luanti client.
* `vcs`: `short` but with `repo`.
@@ -232,8 +253,8 @@ Format query parameters:
* `url`: download URL
* `commit`: commit hash or null
* `downloads`: number of downloads
* `min_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* `max_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* `min_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `max_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `size`: size of zip file, in bytes.
* `package`
* `author`: author username
@@ -242,8 +263,8 @@ Format query parameters:
* GET `/api/updates/` (Look-up table)
* Returns a look-up table from package key (`author/name`) to latest release id
* Query arguments
* `protocol_version`: Only show packages supported by this Minetest protocol version.
* `engine_version`: Only show packages supported by this Minetest engine version, eg: `5.3.0`.
* `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
* GET `/api/packages/<username>/<name>/releases/` (List)
* Returns array of release dictionaries, see above, but without package info.
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
@@ -258,7 +279,7 @@ Format query parameters:
* For zip upload release creation:
* `file`: multipart file to upload, like `<input type="file" name="file">`.
* `commit`: (Optional) Source Git commit hash, for informational purposes.
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
* You can set min and max Luanti Versions [using the content's .conf file](/help/package_config/).
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
* Requires authentication.
* Deletes release.
@@ -267,7 +288,7 @@ Examples:
```bash
# Create release from Git
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{
"method": "git",
@@ -278,17 +299,17 @@ curl -X POST https://content.minetest.net/api/packages/username/name/releases/ne
}'
# Create release from zip upload
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip
# Create release from zip upload with commit hash
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F commit="8ef74deec170a8ce789f6055a59d43876d16a7ea" -F file=@path/to/file.zip
# Delete release
curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/3/ \
curl -X DELETE https://content.luanti.org/api/packages/username/name/releases/3/ \
-H "Authorization: Bearer YOURTOKEN"
```
@@ -329,26 +350,26 @@ Examples:
```bash
# Create screenshot
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png
# Create screenshot and set it as the cover image
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
# Delete screenshot
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
curl -X DELETE https://content.luanti.org/api/packages/username/name/screenshots/3/ \
-H "Authorization: Bearer YOURTOKEN"
# Reorder screenshots
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/order/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]"
# Set cover image
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/cover-image/ \
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/cover-image/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{ 'cover_image': 123 }"
```
@@ -458,7 +479,7 @@ Supported query parameters:
## Collections
* GET `/api/collections/`
* Query args:
* Query args:
* `author`: collection author username.
* `package`: collections that contain the package.
* Returns JSON array of collection entries:
@@ -468,7 +489,7 @@ Supported query parameters:
* `short_description`
* `created_at`: creation time in iso format.
* `private`: whether collection is private, boolean.
* `package_count`: number of packages, integer.
* `package_count`: number of packages, integer.
* GET `/api/collections/<username>/<name>/`
* Returns JSON object for collection:
* `author`: author username.
@@ -498,7 +519,7 @@ Supported query parameters:
### Content Warnings
* GET `/api/content_warnings/` ([View](/api/content_warnings/))
* List of objects with
* List of objects with
* `name`: technical name
* `title`: human-readable title
* `description`: tag description or null
@@ -506,14 +527,14 @@ Supported query parameters:
### Licenses
* GET `/api/licenses/` ([View](/api/licenses/))
* List of objects with:
* List of objects with:
* `name`
* `is_foss`: whether the license is foss
### Minetest Versions
### Luanti Versions
* GET `/api/minetest_versions/` ([View](/api/minetest_versions/))
* List of objects with:
* List of objects with:
* `name`: Version name.
* `is_dev`: boolean, is dev version.
* `protocol_version`: protocol version number.
@@ -521,7 +542,7 @@ Supported query parameters:
### Languages
* GET `/api/languages/` ([View](/api/languages/))
* List of objects with:
* List of objects with:
* `id`: language code.
* `title`: native language name.
* `has_contentdb_translation`: whether ContentDB has been translated into this language.
@@ -552,13 +573,11 @@ Supported query parameters:
* `pop_txp`: popular textures
* `pop_game`: popular games
* `high_reviewed`: highest reviewed
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
* `featured`: featured games
* GET `/api/cdb_schema/` ([View](/api/cdb_schema/))
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
* See [JSON Schema Reference](https://json-schema.org/).
* POST `/api/hypertext/`
* Converts HTML or Markdown to [Minetest Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
* Converts HTML or Markdown to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* Post data: HTML or Markdown as plain text.
* Content-Type: `text/html` or `text/markdown`.

View File

@@ -8,8 +8,8 @@ Expand on the title with the short description. You have a limited number
of characters, use them wisely!
```ini
# Bad, we know this is a mod for Minetest. Doesn't give much information other than "food"
description = The food mod for Minetest
# Bad, we know this is a mod for Luanti. Doesn't give much information other than "food"
description = The food mod for Luanti
# Much better, says what is actually in this mod!
description = Adds soup, cakes, bakes and juices
```
@@ -20,7 +20,7 @@ A good thumbnail goes a long way to making a package more appealing. It's one of
a user sees before clicking on your package. Make sure it's possible to tell what a
thumbnail is when it's small.
For a preview of what your package will look like inside Minetest, see
For a preview of what your package will look like inside Luanti, see
Edit Package > Screenshots.
## Screenshots
@@ -36,7 +36,7 @@ The target audience of your package page is end users.
The long description should explain what your package is about,
why the user should choose it, and how to use it if they download it.
[NodeCore](https://content.minetest.net/packages/Warr1024/nodecore/) is a good
[NodeCore](https://content.luanti.org/packages/Warr1024/nodecore/) is a good
example of what to do. For inspiration, you might want to look at how games on
Steam write their descriptions.
@@ -55,20 +55,20 @@ The following are redundant and should probably not be included:
* API reference (unless your mod is a library only)
* Development instructions for your package (this should be in the repo's README)
* Screenshots that are already uploaded (unless you want to embed a recipe image in a specific place)
* Note: you should avoid images in the long description as they won't be visible inside Minetest,
* Note: you should avoid images in the long description as they won't be visible inside Luanti,
when support for showing the long description is added.
## Localize / Translate your package
According to Google Play, 64% of Minetest Android users don't have English as their main language.
According to Google Play, 64% of Luanti Android users don't have English as their main language.
Adding translation support to your package increases accessibility. Using content translation, you
can also translate your ContentDB page. See Edit Package > Translation for more information.
<p>
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
{{ _("Translation - Minetest Modding Book") }}
{{ _("Translation - Luanti Modding Book") }}
</a>
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
<a class="btn btn-primary" href="https://api.luanti.org/translations/#translating-content-meta">
{{ _("Translating content meta - lua_api.md") }}
</a>
</p>

View File

@@ -6,7 +6,7 @@ your client to use new flags.
## Flags
Minetest allows you to specify a comma-separated list of flags to hide in the
Luanti allows you to specify a comma-separated list of flags to hide in the
client:
```
@@ -17,7 +17,7 @@ A flag can be:
* `nonfree`: can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation.
* `wip`: packages marked as Work in Progress
* `wip`: packages marked as Work in Progress
* `deprecated`: packages marked as Deprecated
* A content warning, given below.
* `*`: hides all content warnings.
@@ -33,8 +33,8 @@ without making a release.
Packages with mature content will be tagged with a content warning based
on the content type.
* `alcohol_tobacco`: alcohol or tobacco.
* `bad_language`: swearing.
* `drugs`: drugs or alcohol.
* `gambling`
* `gore`: blood, etc.
* `horror`: shocking and scary content.

View File

@@ -48,7 +48,7 @@ It's common to do this in README.md or LICENSE.md like so:
* conquer_arrow_*.png from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC0 1.0.
* conquer_arrow.b3d from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC-BY-SA 3.0.
* conquer_arrow_head.png from MTG, CC-BY-SA 3.0.
* health_*.png from [Gauges](https://content.minetest.net/packages/Calinou/gauges/) by Calinou, CC0.
* health_*.png from [Gauges](https://content.luanti.org/packages/Calinou/gauges/) by Calinou, CC0.
```
if you have a lot of media, then you can split it up by author like so:
@@ -75,7 +75,7 @@ Your Name, CC BY-SA 4.0:
* [Kenney game assets](https://www.kenney.nl/assets) - everything
* [Free Sound](https://freesound.org/) - sounds
* [PolyHaven](https://polyhaven.com/) - 3d models and textures.
* Other Minetest mods/games
* Other Luanti mods/games
Don't assume the author has correctly licensed their work.
Make sure they have clearly indicated the source in a list [like above](#list-the-sources-of-your-media).
@@ -141,7 +141,7 @@ permanent bans.
## Where can I get help?
[Join](https://www.minetest.net/get-involved/) IRC, Matrix, or Discord to ask for help.
[Join](https://www.luanti.org/get-involved/) IRC, Matrix, or Discord to ask for help.
In Discord, there are the #assets or #contentdb channels. In IRC or Matrix, you can just ask in the main channels.
If your package is already on ContentDB, you can open a thread.

View File

@@ -48,11 +48,11 @@ There are a number of methods:
* [Webhooks](/help/release_webhooks/): you can configure your Git host to send a webhook to ContentDB, and create an update immediately.
* the [API](/help/api/): This is especially powerful when combined with CI/CD and other API endpoints.
### How do I learn how to make mods and games for Minetest?
### How do I learn how to make mods and games for Luanti?
You should read
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest.
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
### How do I install something from here?

View File

@@ -7,10 +7,10 @@ title: Featured Packages
## What are Featured Packages?
Featured Packages are shown at the top of the ContentDB homepage. In the future,
featured packages may be shown inside the Minetest client.
featured packages may be shown inside the Luanti client.
The purpose is to promote content that demonstrates a high quality of what is
possible in Minetest. The selection should be varied, and should vary over time.
possible in Luanti. The selection should be varied, and should vary over time.
The featured content should be content that we are comfortable recommending to
a first time player.
@@ -47,7 +47,7 @@ other packages to be featured, or for another reason.
* MUST: Be 100% free and open source (as marked as Free on ContentDB).
* MUST: Work out-of-the-box (no weird setup or settings required).
* MUST: Be compatible with the latest stable Minetest release.
* MUST: Be compatible with the latest stable Luanti release.
* SHOULD: Use public source control (such as Git).
* SHOULD: Have at least 3 reviews, and be largely positive.
@@ -94,7 +94,7 @@ is available.
### Usability
* MUST: Unsupported mapgens are disabled in game.conf.
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Minetest) wouldn't get completely
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Luanti) wouldn't get completely
stuck within the first 5 minutes of playing.
* SHOULD: Have good documentation. This may include one or more of:
* A craftguide, or other in-game learning system

View File

@@ -11,6 +11,6 @@ You can follow updates from ContentDB in your RSS feed reader. If in doubt, copy
Follow new releases for a package:
```
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.atom
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.json
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.atom
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.json
```

View File

@@ -17,7 +17,7 @@ You can use `unsupported_games` to specify games that your package doesn't work
with, which is useful for overriding ContentDB's automatic detection.
Both of these are comma-separated lists of game technical ids. Any `_game`
suffixes are ignored, just like in Minetest.
suffixes are ignored, just like in Luanti.
supported_games = minetest_game, repixture
unsupported_games = lordofthetest, nodecore, whynot

View File

@@ -1,5 +1,5 @@
title: How to install mods, games, and texture packs
description: A guide to installing mods, games, and texture packs in Minetest.
description: A guide to installing mods, games, and texture packs in Luanti.
## Installing from the main menu (recommended)
@@ -7,8 +7,8 @@ description: A guide to installing mods, games, and texture packs in Minetest.
1. Open the mainmenu
2. Go to the Content tab and click "Browse online content".
If you don't see this, then you need to update Minetest to v5.
3. Search for the package you want to install, and click "Install".
If you don't see this, then you need to update Luanti to v5.
3. Search for the package you want to install, and click "Install".
4. When installing a mod, you may be shown a dialog about dependencies here.
Make sure the base game dropdown box is correct, and then click "Install".
@@ -16,7 +16,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
<div class="col-md-6">
<figure>
<a href="/static/installing_content_tab.png">
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in minetest">
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in Luanti">
</a>
<figcaption class="text-muted ps-1">
1. Click Browser Online Content in the content tab.
@@ -26,7 +26,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
<div class="col-md-6">
<figure>
<a href="/static/installing_cdb_dialog.png">
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in minetest">
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in Luanti">
</a>
<figcaption class="text-muted ps-1">
2. Search for the package and click "Install".
@@ -38,7 +38,7 @@ description: A guide to installing mods, games, and texture packs in Minetest.
Troubleshooting:
* I can't find it in the ContentDB dialog (Browse online content)
* Make sure that you're on the latest version of Minetest.
* Make sure that you're on the latest version of Luanti.
* Are you using Android? Packages with content warnings are hidden by default on android,
you can show them by removing `android_default` from the `contentdb_flag_blacklist` setting.
* Does the webpage show "Non-free" warnings? Non-free content is hidden by default from all clients,
@@ -51,14 +51,14 @@ Troubleshooting:
1. Mods: Enable the content using "Select Mods" when selecting a world.
2. Games: choose a game when making a world.
3. Texture packs: Content > Select pack > Click enable.
3. Texture packs: Content > Select pack > Click enable.
<div class="row mt-5">
<div class="col-md-6">
<figure>
<a href="/static/installing_select_mods.png">
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Minetest">
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Luanti">
</a>
<figcaption class="text-muted ps-1">
Enable mods using the Select Mods dialog.
@@ -76,7 +76,7 @@ Troubleshooting:
3. Find the user data directory.
In 5.4.0 and above, you can click "Open user data directory" in the Credits tab.
Otherwise:
* Windows: whereever you extracted or installed Minetest to.
* Windows: wherever you extracted or installed Luanti to.
* Linux: usually `~/.minetest/`
4. Open or create the folder for the type of content (`mods`, `games`, or `textures`)
5. Git clone there

View File

@@ -13,7 +13,7 @@ and they will be subject to limited promotion.
**ContentDB does not allow certain non-free licenses, and will limit the promotion
of packages with non-free licenses.**
Minetest is free and open source software, and is only as big as it is now
Luanti is free and open source software, and is only as big as it is now
because of this. It's pretty amazing you can take nearly any published mod and modify it
to how you like - add some features, maybe fix some bugs - and then share those
modifications without the worry of legal issues. The project, itself, relies on open
@@ -24,9 +24,9 @@ If you have played nearly any game with a large modding scene, you will find
that most mods are legally ambiguous. A lot of them don't even provide the
source code to allow you to bug fix or extend as you need.
Limiting the promotion of problematic licenses helps Minetest avoid ending up in
Limiting the promotion of problematic licenses helps Luanti avoid ending up in
such a state. Licenses that prohibit redistribution or modification are
completely banned from ContentDB and the Minetest forums. Other non-free licenses
completely banned from ContentDB and the Luanti forums. Other non-free licenses
will be subject to limited promotion - they won't be shown by default in
the client.
@@ -37,7 +37,7 @@ you spread it.
## What's so bad about licenses that forbid commercial use?
Please read [reasons not to use a Creative Commons -NC license](https://freedomdefined.org/Licenses/NC).
Here's a quick summary related to Minetest content:
Here's a quick summary related to Luanti content:
1. They make your work incompatible with a growing body of free content, even if
you do want to allow derivative works or combinations.
@@ -68,7 +68,7 @@ Users can opt in to showing non-free software, if they wish:
The [`platform_default` flag](/help/content_flags/) is used to control what content
each platforms shows. It doesn't hide anything on Desktop, but hides all mature
content on Android. You may wish to remove all text from that setting completely,
content on Android. You may wish to remove all text from that setting completely,
leaving it blank. See [Content Warnings](/help/content_flags/#content-warnings)
for information on mature content.

View File

@@ -27,7 +27,7 @@ ContentDB supports the Authorization Code OAuth2 method.
Get the user to open the following URL in a web browser:
```
https://content.minetest.net/oauth/authorize/
https://content.luanti.org/oauth/authorize/
?response_type=code
&client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URL}
@@ -52,7 +52,7 @@ Next, you'll need to exchange the auth for an access token.
Do this by making a POST request to the `/oauth/token/` API:
```bash
curl -X POST https://content.minetest.net/oauth/token/ \
curl -X POST https://content.luanti.org/oauth/token/ \
-F grant_type=authorization_code \
-F client_id="CLIENT_ID" \
-F client_secret="CLIENT_SECRET" \
@@ -98,6 +98,6 @@ Possible errors:
Next, you should check the access token works by getting the user information:
```bash
curl https://content.minetest.net/api/whoami/ \
curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN"
```

View File

@@ -42,8 +42,8 @@ ContentDB understands the following information:
* `description` - A short description to show in the client.
* `depends` - Comma-separated hard dependencies.
* `optional_depends` - Comma-separated soft dependencies.
* `min_minetest_version` - The minimum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
* `max_minetest_version` - The maximum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
* `min_minetest_version` - The minimum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
* `max_minetest_version` - The maximum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
and for mods only:
@@ -68,7 +68,7 @@ It should be a JSON dictionary with one or more of the following optional keys:
* `tags`: List of tag names, see [/api/tags/](/api/tags/).
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
* `license`: A license name, see [/api/licenses/](/api/licenses/).
* `media_license`: A license name.
* `media_license`: A license name.
* `long_description`: Long markdown description.
* `repo`: Source repository (eg: Git).
* `website`: Website URL.
@@ -106,11 +106,11 @@ See [Git Update Detection](/help/update_config/).
You can also use [GitLab/GitHub webhooks](/help/release_webhooks/) or the [API](/help/api/)
to create releases.
### Min and Max Minetest Versions
### Min and Max Luanti Versions
<a name="min_max_versions" />
When creating a release, the `.conf` file will be read to determine what Minetest
When creating a release, the `.conf` file will be read to determine what Luanti
versions the release supports. If the `.conf` doesn't specify, then it is assumed
that it supports all versions.

View File

@@ -20,7 +20,7 @@ The process is as follows:
3. The git host posts a webhook notification to ContentDB, using the API token assigned to it.
4. ContentDB checks the API token and issues a new release.
* If multiple packages match, then only the first will have a release created.
### Branch filtering
By default, "New commit" or "push" based webhooks will only work on "master"/"main" branches.
@@ -39,7 +39,7 @@ Tag-based webhooks are accepted on any branch.
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated.
3. Go to the GitLab repository's settings > Webhooks > Add Webhook.
4. Set the payload URL to `https://content.minetest.net/github/webhook/`
4. Set the payload URL to `https://content.luanti.org/github/webhook/`
5. Set the content type to JSON.
6. Set the secret to the access token that you copied.
7. Set the events
@@ -53,7 +53,7 @@ Tag-based webhooks are accepted on any branch.
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated.
3. Go to the GitLab repository's settings > Webhooks.
4. Set the URL to `https://content.minetest.net/gitlab/webhook/`
4. Set the URL to `https://content.luanti.org/gitlab/webhook/`
6. Set the secret token to the ContentDB access token that you copied.
7. Set the events
* If you want a rolling release, choose "Push events".
@@ -67,5 +67,5 @@ Tag-based webhooks are accepted on any branch.
See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation.
From the Git repository, you can set the min/max Minetest versions, which files are included,
From the Git repository, you can set the min/max Luanti versions, which files are included,
and update the package meta.

View File

@@ -33,4 +33,4 @@ downloaded from that IP.
You can see all scores using the [scores REST API](/api/scores/), or by
using the [Prometheus metrics](/help/metrics/) endpoint.
Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
Consider [suggesting improvements](https://github.com/luanti-org/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).

View File

@@ -39,5 +39,5 @@ Clicking "Save" on "Update Settings" will mark a package as up-to-date.
See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation.
From the Git repository, you can set the min/max Minetest versions, which files are included,
From the Git repository, you can set the min/max Luanti versions, which files are included,
and update the package meta.

View File

@@ -1,25 +1,6 @@
title: WTFPL is a terrible license
toc: False
<div id="warning" class="alert alert-warning">
<span class="icon_message"></span>
Please reconsider the choice of WTFPL as a license.
<script>
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
var params = new URLSearchParams(location.search);
var r = params.get("r");
if (r) {
document.write("<a class='alert_right button' href='" + r + "'>Okay</a>");
} else {
document.getElementById("warning").style.display = "none";
}
</script>
</div>
The use of WTFPL as a license is discouraged for multiple reasons.
* **No Warranty disclaimer:** This could open you up to being sued.<sup>[1]</sup>
@@ -37,4 +18,4 @@ license, saying:<sup>[3]</sup>
1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html)
2. [FSF](https://www.gnu.org/licenses/license-list.en.html)
3. [OSI](https://opensource.org/minutes20090304)
3. [OSI](https://opensource.org/meeting-minutes/minutes20090304)

View File

@@ -1,22 +1,5 @@
title: Package Inclusion Policy and Guidance
## 0. Overview
ContentDB is for the community, and as such listings should be useful to the
community. To help with this, there are a few rules to improve the quality of
the listings and to combat abuse.
* **No inappropriate content.** <sup>2.1</sup>
* **Content must be playable/useful, but not necessarily finished.** <sup>2.2</sup>
* **Don't use the name of another mod unless your mod is a fork or reimplementation.** <sup>3</sup>
* **Licenses must allow derivatives, redistribution, and must not discriminate.** <sup>4</sup>
* **Don't put promotions or advertisements in any package metadata.** <sup>5</sup>
* **Don't manipulate package placement using reviews or downloads.** <sup>6</sup>
* **Screenshots must not be misleading.** <sup>7</sup>
* **The ContentDB admin reserves the right to remove packages for any reason**,
including ones not covered by this document, and to ban users who abuse
this service. <sup>1</sup>
## 1. General
@@ -26,33 +9,53 @@ including ones not covered by this document, and to ban users who abuse this ser
## 2. Accepted Content
### 2.1. Acceptable Content
### 2.1. Mature Content
Sexually-orientated content is not permitted.
If in doubt at what this means, [contact us by raising a report](/report/).
See the [Terms of Service](/terms/) for a full list of prohibited content.
Mature content is permitted providing that it is labelled correctly.
See [Content Flags](/help/content_flags/).
Other mature content is permitted providing that it is labelled with the applicable
[content warning](/help/content_flags/).
### 2.2. State of Completion
### 2.2. Useful Content / State of Completion
ContentDB should only currently contain playable content - content which is
sufficiently complete to be useful to end-users. It's fine to add stuff which is
still a Work in Progress (WIP) as long as it adds sufficient value; Note that
this doesn't mean that you should add a thing you started working on yesterday,
it's worth adding all the basic stuff to make your package useful.
ContentDB is for playable and useful content - content which is sufficiently
complete to be useful to end-users.
You should make sure to mark Work in Progress stuff as such in the "maintenance
status" column, as this will help advise players.
It's fine to add stuff which is still a Work in Progress (WIP) as long as it
adds sufficient value. You must make sure to mark Work in Progress stuff as
such in the "maintenance status" dropdown, as this will help advise players.
Adding non-player facing mods, such as libraries and server tools, is perfectly
fine and encouraged. ContentDB isn't just for player-facing things, and adding
libraries allows them to be installed when a mod depends on it.
fine and encouraged. ContentDB isn't just for player-facing things and adding
libraries allows Luanti to automatically install dependencies.
### 2.3. Language
We require packages to be in English with (optional) client-side translations for
other languages. This is because Luanti currently requires English as the base language
([Issue to change this](https://github.com/luanti-org/luanti/issues/6503)).
Your package's title and short description must be in English. You can use client-side
translations to [translate content meta](https://api.luanti.org/translations/#translating-content-meta).
### 2.4. Attempt to contribute before forking
You should attempt to contribute upstream before forking a package. If you choose
to fork, you should have a justification (different objectives, maintainer is unavailable, etc).
You should use a different title and make it clear in the long description what the
benefit of your fork is over the original package.
### 2.5. Copyright and trademarks
Your package must not violate copyright or trademarks. You should avoid the use of
trademarks in the package's title or short description. If you do use a trademark,
ensure that you phrase it in a way that does not imply official association or
endorsement.
## 3. Technical Names
### 3.1 Right to a name
### 3.1. Right to a Name
A package uses a name when it has that name or contains a mod that uses that name.
@@ -70,23 +73,46 @@ to change the name of the package, or your package won't be accepted.
We reserve the right to issue exceptions for this where we feel necessary.
### 3.2. Mod Forks and Reimplementations
### 3.2. Forks and Reimplementations
An exception to the above is that mods are allowed to have the same name as a
mod if it's a fork of that mod (or a close reimplementation). In real terms, it
should be possible to use the new mod as a drop-in replacement.
must be possible to use the new mod as a drop-in replacement.
We reserve the right to decide whether a mod counts as a fork or
reimplementation of the mod that owns the name.
### 3.3. Game Mod Namespacing
New mods introduced by a game must have a unique common prefix to avoid conflicts with
other games and standalone mods. For example, the NodeCore game's first-party mods all
start with `nc_`: `nc_api`, `nc_doors`.
You may include existing or standard mods in your game without renaming them to use the
namespace. For example, NodeCore could include the `awards` mod without needing to rename it.
Standalone mods may not use a game's namespace unless they have been given permission by
the game's author.
The exception given by 3.2 also applies to game namespaces - you may use another game's
prefix if your game is a fork.
## 4. Licenses
### 4.1. Allowed Licenses
### 4.1. License file
You must have a LICENSE, LICENSE.txt, or LICENSE.md file describing the licensing of your package.
Please ensure that you correctly credit any resources (code, assets, or otherwise)
that you have used in your package. For help on doing copyright correctly, see
the [Copyright help page](/help/copyright/).
that you have used in your package.
You may use lowercase or include a suffix in the filename (ie: `license-code.txt`). If
you are making a game or modpack, your top level license file may just be a summary or
refer to the license files of individual components.
For help on doing copyright correctly, see the [Copyright help page](/help/copyright/).
### 4.2. Allowed Licenses
**The use of licenses that do not allow derivatives or redistribution is not
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
@@ -96,18 +122,18 @@ of the content on servers or singleplayer is also not permitted.**
However, closed sourced licenses are allowed if they allow the above.
If the license you use is not on the list then please select "Other", and we'll
get around to adding it. We tend to reject custom/untested licenses, and
reserve the right to decide whether a license should be included.
get around to adding it. We reject custom/untested licenses and reserve the right
to decide whether a license should be included.
Please note that the definitions of "free" and "non-free" is the same as that
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
### 4.2. Recommended Licenses
### 4.3. Recommended Licenses
It is highly recommended that you use a Free and Open Source software (FOSS)
license. FOSS licenses result in a sharing community and will increase the
number of potential users your package has. Using a closed source license will
result in your package not being shown in Minetest by default. See the help page
result in your package not being shown in Luanti by default. See the help page
on [non-free licenses](/help/non_free/) for more information.
It is recommended that you use a proper license for code with a warranty
@@ -150,10 +176,14 @@ Doing so may result in temporary or permanent suspension from ContentDB.
## 7. Screenshots
1. **Screenshots must not violate copyright.** You should have the rights to the
screenshot.
1. We require all packages to have at least one screenshot. For packages without visual
content, we recommend making a symbolic image with icons, graphics, or text to depict
the package.
2. **Screenshots must depict the actual content of the package in some way, and
2. **Screenshots must not violate copyright.** This means don't just copy images
from Google search, see [the copyright guide](/help/copyright/).
3. **Screenshots must depict the actual content of the package in some way, and
not be misleading.**
Do not use idealized mockups or blender concept renders if they do not
@@ -169,20 +199,9 @@ Doing so may result in temporary or permanent suspension from ContentDB.
will look like in a typical/realistic game scene, but should be "in the
background" only as far as possible.
3. **Screenshots must only contain content appropriate for the Content Warnings of
4. **Screenshots must only contain content appropriate for the Content Warnings of
the package.**
4. **Screenshots should be MOSTLY in-game screenshots, if applicable.** Some
alterations on in-game screenshots are okay, such as collages, added text,
some reasonable compositing.
Don't just use one of the textures from the package; show it in-situ as it
actually looks in the game.
5. **Packages should have a screenshot when reasonably applicable.**
6. **Screenshots should be of reasonable dimensions.** We recommend using 1920x1080.
## 8. Security

View File

@@ -2,7 +2,7 @@ title: Privacy Policy
---
Last Updated: 2024-04-30
([View updates](https://github.com/minetest/contentdb/commits/master/app/flatpages/privacy_policy.md))
([View updates](https://github.com/luanti-org/contentdb/commits/master/app/flatpages/privacy_policy.md))
## What Information is Collected
@@ -56,7 +56,7 @@ Please avoid giving other personal information as we do not want it.
* Only the admin has access to the HTTP requests.
The logs may be shared with others to aid in debugging, but care will be taken to remove any personal information.
* Encrypted backups may be shared with selected Minetest staff members (moderators + core devs).
* Encrypted backups may be shared with selected Luanti staff members (moderators + core devs).
The keys and the backups themselves are given to different people,
requiring at least two staff members to read a backup.
* Email addresses are visible to moderators and the admin.

View File

@@ -1,15 +0,0 @@
title: Rules
The following are the rules for user behaviour on ContentDB, including reviews,
threads, comments, and profiles. For packages, see the
[Package Inclusion Policy](/policy_and_guidance/).
1. **Be respectful:** attacks towards any person or group, slurs,
trolling/baiting, and other toxic behavior are not welcome.
2. **Assume good faith:** communication over the Internet is hard, try to assume
good faith when eg: responding to reviews.
3. **No sexual content** and ensure you keep discussion appropriate given the
package's [content warnings](/help/content_flags/).
You can report things by clicking [report](/report/) in the footer of pages you
want to report.

133
app/flatpages/terms.md Normal file
View File

@@ -0,0 +1,133 @@
title: Terms of Service
Also see the [Package Inclusion Policy](/policy_and_guidance/).
## Content
### Prohibited content
You must not post/transmit anything which is illegal under the laws in any part of the United Kingdom.
You must not (or use the service to) facilitate or commit any offence under the laws in any part of the United Kingdom.
This includes, in particular, terrorism content (as set out in Schedule 5, Online Safety Act 2023),
child sexual exploitation and abuse content (as set out in Schedule 6, Online Safety Act 2023), and
content that amounts to an offence specified in Schedule 7, Online Safety Act 2023.
Prohibited content includes:
* Pornographic content. This includes content of such a nature that it is reasonable to assume that it was produced
solely or principally for the purpose of sexual arousal.
* Content which encourages, promotes or provides instructions for suicide
* Content which encourages, promotes or provides instructions for an act of deliberate self-injury
* Content which encourages, promotes or provides instructions for an eating disorder or behaviours associated with an eating disorder
* Content which is abusive and which targets any of the following characteristics: race, religion, sex,
sexual orientation, disability, gender reassignment.
* Content which incites hatred against people:
* of a particular race, religion, sex or sexual orientation
* who have a disability
* who have the characteristic of gender reassignment
* Content which encourages, promotes or provides instructions for an act of serious violence against a person
* Bullying content
* Content which:
* depicts real or realistic serious violence against a person
* depicts the real or realistic serious injury of a person in graphic detail
* Content which:
* depicts real or realistic serious violence against an animal
* depicts the real or realistic serious injury of an animal in graphic detail
* realistically depicts serious violence against a fictional creature or the serious injury of a fictional
creature in graphic detail
* Content which encourages, promotes or provides instructions for a challenge or stunt highly likely to result in
serious injury to the person who does it or to someone else
* Content which encourages a person to ingest, inject, inhale or in any other way self-administer:
* a physically harmful substance
* a substance in such a quantity as to be physically harmful
### Protecting users from illegal content
We provide this service free of charge, and on the basis that we may:
* take down, or restrict access to, anything that you generate, upload or share; and
* suspend or ban you from using all or part of the service
if we think that this is reasonable to protect you, other users, the service, or us. This applies, in particular,
to prohibited content.
If we are alerted by a person to the presence of any illegal content, or we become aware of it in any other way,
we will swiftly take down that content.
To minimise the length of time for which any illegal content within the scope of the Online Safety Act 2023 is present:
* in respect of terrorism content, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
* in respect of child sexual exploitation or abuse content, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
* in respect of other content that amounts to an offence specified in Schedule 7, Online Safety Act 2023, we offer an easy-to-access and use reporting function and will swiftly remove such content when we become aware of it.
### Protecting children
We protect all children from the kinds of content listed in "Prohibited Content" by:
* prohibiting that type of content from our service; and
* swiftly taking down that content, if we are alerted by a person to its presence, or we become aware of it in any other way.
### Proactive technology
We do not use proactive technology to detect illegal content.
## Limitation of Liability
THE SERVICE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL WE BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SERVICE OR THE USE OR OTHER DEALINGS IN THE SERVICE.
We reserve the right to ban or suspend your account, or take down your content, for any reason.
## Jurisdiction and Governing Law
This service is subject to the jurisdiction of the United Kingdom.
## Complaints
### Reporting content
You may report content by clicking the report flag next to a comment or "Report" on the page containing the content.
You can also make reports by [contacting the admin]({{ admin_contact_url }}).
### Complaints and Appeals
You may send a complaint / request an appeal by [contacting the admin]({{ admin_contact_url }}).
### Your right to bring a claim
This clause applies only to users within the United Kingdom.
The Online Safety Act 2023 says that you have a right to bring a claim for breach of contract if:
* anything that you generate, upload or share is taken down, or access to it is restricted, in breach of the terms of service, or
* you are suspended or banned from using the service in breach of the terms of service.
This does not apply to emails, SMS messages, MMS messages, one-to-one live aural communications,
comments and reviews (together with any further comments on such comments or reviews), or content which identifies
you as a user (e.g. a user name or profile picture).
Whether or not a contract exists between you and us is a question of fact. If we do not have a contractual
relationship with you in respect of the service, there can be no breach of contract and, as such, this cannot apply.
It is for a court to determine:
- if there is a contract between you and us and, if so, its terms
- if there has been a breach by us of that contract
- if that breach has caused you any recoverable loss
- the size (e.g. value) of your loss
This clause is subject to "Limitation of liability" and "Jurisdiction".
## Acknowledgements
This terms of service was written based on [a template](https://onlinesafetyact.co.uk/online_safety_act_terms/)
created by Neil Brown, CC BY-SA 4.0.

View File

@@ -96,7 +96,7 @@ def _get_approval_statistics(entries: list[AuditLogEntry], start_date: Optional[
(end_date is None or entry.created_at <= end_date)))
info.is_in_range = info.is_in_range or is_in_range
new_state = get_state(entry.title)
new_state = get_state(entry.title.replace("", "") + (entry.description or ""))
if new_state == info.state:
continue

View File

@@ -174,12 +174,6 @@ class GameSupport:
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
if package.id_ in visited:
first_idx = visited.index(package.id_)
visited = visited[first_idx:]
err = f"Dependency cycle detected: {' -> '.join(visited)} -> {package.id_}"
for id_ in visited:
package2 = self.get(id_)
package2.add_error(err)
return None
if package.type == PackageType.GAME:

View File

@@ -46,6 +46,9 @@ class PackageValidationNote:
self.buttons.append((url, label))
return self
def __repr__(self):
return str(self.message)
def is_package_name_taken(normalised_name: str) -> bool:
return Package.query.filter(
@@ -107,8 +110,7 @@ def validate_package_for_approval(package: Package) -> List[PackageValidationNot
# Don't bother validating any more until we have a release
return retval
if (package.type == PackageType.GAME or package.type == PackageType.TXP) and \
package.screenshots.count() == 0:
if package.screenshots.count() == 0:
danger(lazy_gettext("You need to add at least one screenshot."))
missing_deps = package.get_missing_hard_dependencies_query().all()

View File

@@ -69,6 +69,19 @@ ALLOWED_FIELDS = {
"translation_url": str,
}
NULLABLE = {
"tags",
"content_warnings",
"repo",
"website",
"issue_tracker",
"issueTracker",
"forums",
"video_url",
"donate_url",
"translation_url",
}
ALIASES = {
"short_description": "short_desc",
"issue_tracker": "issueTracker",
@@ -86,11 +99,13 @@ def is_int(val):
def validate(data: dict):
for key, value in data.items():
if value is not None:
if value is None:
check(key in NULLABLE, f"{key} must not be null")
else:
typ = ALLOWED_FIELDS.get(key)
check(typ is not None, key + " is not a known field")
check(typ is not None, f"{key} is not a known field")
if typ != AnyType:
check(isinstance(value, typ), key + " must be a " + typ.__name__)
check(isinstance(value, typ), f"{key} must be a " + typ.__name__)
if "name" in data:
name = data["name"]
@@ -102,12 +117,12 @@ def validate(data: dict):
value = data.get(key)
if value is not None:
check(value.startswith("http://") or value.startswith("https://"),
key + " must start with http:// or https://")
check(validators.url(value), key + " must be a valid URL")
f"{key} must start with http:// or https://")
check(validators.url(value), f"{key} must be a valid URL")
def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, data: dict,
reason: str = None):
reason: str = None) -> bool:
if not package.check_perm(user, Permission.EDIT_PACKAGE):
raise LogicError(403, lazy_gettext("You don't have permission to edit this package"))
@@ -121,6 +136,9 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
for alias, to in ALIASES.items():
if alias in data:
if to in data and data[to] != data[alias]:
raise LogicError(403, f"Aliased field ({alias}) does not match new field ({to})")
data[to] = data[alias]
validate(data)
@@ -169,7 +187,6 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
package.provides.append(m)
if "tags" in data:
old_tags = list(package.tags)
package.tags.clear()
for tag_id in (data["tags"] or []):
if is_int(tag_id):
@@ -192,9 +209,14 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
raise LogicError(400, "Unknown warning: " + warning_id)
package.content_warnings.append(warning)
if not was_new:
was_modified = was_new
if was_new:
msg = f"Created package {package.author.username}/{package.name}"
add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
else:
after_dict = package.as_dict("/")
diff = diff_dictionaries(before_dict, after_dict)
was_modified = len(diff) > 0
if reason is None:
msg = "Edited {}".format(package.title)
@@ -208,6 +230,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
severity = AuditSeverity.NORMAL if user in package.maintainers else AuditSeverity.EDITOR
add_audit_log(severity, user, msg, package.get_url("packages.view"), package, json.dumps(diff, indent=4))
db.session.commit()
if was_modified:
db.session.commit()
return package
return was_modified

View File

@@ -23,12 +23,12 @@ from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease
from app.models import PackageRelease, db, Permission, User, Package, LuantiRelease
from app.tasks.importtasks import make_vcs_release, check_zip_release
from app.utils import AuditSeverity, add_audit_log, nonempty_or_none, normalize_line_endings
def check_can_create_release(user: User, package: Package):
def check_can_create_release(user: User, package: Package, name: str):
if not package.check_perm(user, Permission.MAKE_RELEASE):
raise LogicError(403, lazy_gettext("You don't have permission to make releases"))
@@ -37,10 +37,13 @@ def check_can_create_release(user: User, package: Package):
if count >= 5:
raise LogicError(429, lazy_gettext("You've created too many releases for this package in the last 5 minutes, please wait before trying again"))
if PackageRelease.query.filter_by(package_id=package.id, name=name).count() > 0:
raise LogicError(403, lazy_gettext("A release with this name already exists"))
def do_create_vcs_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None):
check_can_create_release(user, package)
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None):
check_can_create_release(user, package, name)
rel = PackageRelease()
rel.package = package
@@ -67,9 +70,9 @@ def do_create_vcs_release(user: User, package: Package, name: str, title: Option
def do_create_zip_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None,
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None,
commit_hash: str = None):
check_can_create_release(user, package)
check_can_create_release(user, package, name)
if commit_hash:
commit_hash = commit_hash.lower()

View File

@@ -17,7 +17,7 @@
import imghdr
import os
from flask_babel import lazy_gettext
from flask_babel import lazy_gettext, LazyString
from app import app
from app.logic.LogicError import LogicError
@@ -35,7 +35,7 @@ def is_allowed_image(data):
return imghdr.what(None, data) in ALLOWED_IMAGES
def upload_file(file, file_type, file_type_desc):
def upload_file(file, file_type: str, file_type_desc: LazyString | str, length: int=10):
if not file or file is None or file.filename == "":
raise LogicError(400, "Expected file")
@@ -62,7 +62,7 @@ def upload_file(file, file_type, file_type_desc):
file.stream.seek(0)
filename = random_string(10) + "." + ext
filename = random_string(length) + "." + ext
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
file.save(filepath)

View File

@@ -1,214 +0,0 @@
# ContentDB
# Copyright (C) rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from functools import partial
from urllib.parse import urljoin
import bleach
from bleach import Cleaner
from bleach.linkifier import LinkifyFilter
from bs4 import BeautifulSoup
from markdown import Markdown
from flask import url_for
from jinja2.utils import markupsafe
from markdown.extensions import Extension
from markdown.inlinepatterns import SimpleTagInlineProcessor
from markdown.inlinepatterns import Pattern
from markdown.extensions.codehilite import CodeHiliteExtension
from xml.etree import ElementTree
# Based on
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
#
# License: MIT
ALLOWED_TAGS = {
"h1", "h2", "h3", "h4", "h5", "h6", "hr",
"ul", "ol", "li",
"p",
"br",
"pre",
"code",
"blockquote",
"strong",
"em",
"a",
"img",
"table", "thead", "tbody", "tr", "th", "td",
"div", "span", "del", "s",
"details",
"summary",
}
ALLOWED_CSS = [
"highlight", "codehilite",
"hll", "c", "err", "g", "k", "l", "n", "o", "x", "p", "ch", "cm", "cp", "cpf", "c1", "cs",
"gd", "ge", "gr", "gh", "gi", "go", "gp", "gs", "gu", "gt", "kc", "kd", "kn", "kp", "kr",
"kt", "ld", "m", "s", "na", "nb", "nc", "no", "nd", "ni", "ne", "nf", "nl", "nn", "nx",
"py", "nt", "nv", "ow", "w", "mb", "mf", "mh", "mi", "mo", "sa", "sb", "sc", "dl", "sd",
"s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il",
]
def allow_class(_tag, name, value):
return name == "class" and value in ALLOWED_CSS
ALLOWED_ATTRIBUTES = {
"h1": ["id"],
"h2": ["id"],
"h3": ["id"],
"h4": ["id"],
"a": ["href", "title", "data-username"],
"img": ["src", "title", "alt"],
"code": allow_class,
"div": allow_class,
"span": allow_class,
"table": ["id"],
}
ALLOWED_PROTOCOLS = {"http", "https", "mailto"}
md = None
def linker_callback(attrs, new=False):
if new:
text = attrs.get("_text")
if not (text.startswith("http://") or text.startswith("https://")):
return None
return attrs
def render_markdown(source):
html = md.convert(source)
cleaner = Cleaner(
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
filters=[partial(LinkifyFilter,
callbacks=[linker_callback] + bleach.linkifier.DEFAULT_CALLBACKS,
skip_tags={"pre", "code"})])
return cleaner.clean(html)
class DelInsExtension(Extension):
def extendMarkdown(self, md):
del_proc = SimpleTagInlineProcessor(r"(\~\~)(.+?)(\~\~)", "del")
md.inlinePatterns.register(del_proc, "del", 200)
ins_proc = SimpleTagInlineProcessor(r"(\+\+)(.+?)(\+\+)", "ins")
md.inlinePatterns.register(ins_proc, "ins", 200)
RE_PARTS = dict(
USER=r"[A-Za-z0-9._-]*\b",
REPO=r"[A-Za-z0-9_]+\b"
)
class MentionPattern(Pattern):
ANCESTOR_EXCLUDES = ("a",)
def __init__(self, config, md):
MENTION_RE = r"(@({USER})(?:\/({REPO}))?)".format(**RE_PARTS)
super(MentionPattern, self).__init__(MENTION_RE, md)
self.config = config
def handleMatch(self, m):
from app.models import User
label = m.group(2)
user = m.group(3)
package_name = m.group(4)
if package_name:
el = ElementTree.Element("a")
el.text = label
el.set("href", url_for("packages.view", author=user, name=package_name))
return el
else:
if User.query.filter_by(username=user).count() == 0:
return None
el = ElementTree.Element("a")
el.text = label
el.set("href", url_for("users.profile", username=user))
el.set("data-username", user)
return el
class MentionExtension(Extension):
def __init__(self, *args, **kwargs):
super(MentionExtension, self).__init__(*args, **kwargs)
def extendMarkdown(self, md):
md.ESCAPED_CHARS.append("@")
md.inlinePatterns.register(MentionPattern(self.getConfigs(), md), "mention", 20)
MARKDOWN_EXTENSIONS = ["fenced_code", "tables", CodeHiliteExtension(guess_lang=False), "toc", DelInsExtension(), MentionExtension()]
MARKDOWN_EXTENSION_CONFIG = {
"fenced_code": {},
"tables": {}
}
def init_markdown(app):
global md
md = Markdown(extensions=MARKDOWN_EXTENSIONS,
extension_configs=MARKDOWN_EXTENSION_CONFIG,
output_format="html")
@app.template_filter()
def markdown(source):
return markupsafe.Markup(render_markdown(source))
def get_headings(html: str):
soup = BeautifulSoup(html, "html.parser")
headings = soup.find_all(["h1", "h2", "h3"])
root = []
stack = []
for heading in headings:
this = {"link": heading.get("id") or "", "text": heading.text, "children": []}
this_level = int(heading.name[1:]) - 1
while this_level <= len(stack):
stack.pop()
if len(stack) > 0:
stack[-1]["children"].append(this)
else:
root.append(this)
stack.append(this)
return root
def get_user_mentions(html: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[data-username]")
return set([x.get("data-username") for x in links])
def get_links(html: str, url: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[href]")
return set([urljoin(url, x.get("href")) for x in links])

112
app/markdown/__init__.py Normal file
View File

@@ -0,0 +1,112 @@
# ContentDB
# Copyright (C) rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Sequence
from bs4 import BeautifulSoup
from jinja2.utils import markupsafe
from markdown_it import MarkdownIt
from markdown_it.common.utils import unescapeAll, escapeHtml
from markdown_it.token import Token
from markdown_it.presets import gfm_like
from mdit_py_plugins.anchors import anchors_plugin
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.util import ClassNotFound
from pygments.formatters.html import HtmlFormatter
from .cleaner import clean_html
from .mention import init_mention
def highlight_code(code, name, attrs):
try:
lexer = get_lexer_by_name(name)
except ClassNotFound:
return None
formatter = HtmlFormatter()
return highlight(code, lexer, formatter)
def render_code(self, tokens: Sequence[Token], idx, options, env):
token = tokens[idx]
info = unescapeAll(token.info).strip() if token.info else ""
langName = info.split(maxsplit=1)[0] if info else ""
if options.highlight:
return options.highlight(
token.content, langName, ""
) or f"<pre><code>{escapeHtml(token.content)}</code></pre>"
return f"<pre><code>{escapeHtml(token.content)}</code></pre>"
gfm_like.make()
md = MarkdownIt("gfm-like", {"highlight": highlight_code})
md.use(anchors_plugin, permalink=True, permalinkSymbol="🔗", max_level=6)
md.add_render_rule("fence", render_code)
init_mention(md)
def render_markdown(source, clean=True):
html = md.render(source)
if clean:
return clean_html(html)
else:
return html
def init_markdown(app):
@app.template_filter()
def markdown(source):
return markupsafe.Markup(render_markdown(source))
def get_headings(html: str):
soup = BeautifulSoup(html, "html.parser")
headings = soup.find_all(["h1", "h2", "h3"])
root = []
stack = []
for heading in headings:
text = heading.find(text=True, recursive=False)
this = {"link": heading.get("id") or "", "text": text, "children": []}
this_level = int(heading.name[1:]) - 1
while this_level <= len(stack):
stack.pop()
if len(stack) > 0:
stack[-1]["children"].append(this)
else:
root.append(this)
stack.append(this)
return root
def get_user_mentions(html: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[data-username]")
return set([x.get("data-username") for x in links])
def get_links(html: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[href]")
return set([x.get("href") for x in links])

97
app/markdown/cleaner.py Normal file
View File

@@ -0,0 +1,97 @@
# ContentDB
# Copyright (C) rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from functools import partial
from bleach import Cleaner
from bleach.linkifier import LinkifyFilter, DEFAULT_CALLBACKS
# Based on
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
#
# License: MIT
ALLOWED_TAGS = {
"h1", "h2", "h3", "h4", "h5", "h6", "hr",
"ul", "ol", "li",
"p",
"br",
"pre",
"code",
"blockquote",
"strong",
"em",
"a",
"img",
"table", "thead", "tbody", "tr", "th", "td",
"div", "span", "del", "s",
"details",
"summary",
"sup",
}
ALLOWED_CSS = [
"highlight", "codehilite",
"hll", "c", "err", "g", "k", "l", "n", "o", "x", "p", "ch", "cm", "cp", "cpf", "c1", "cs",
"gd", "ge", "gr", "gh", "gi", "go", "gp", "gs", "gu", "gt", "kc", "kd", "kn", "kp", "kr",
"kt", "ld", "m", "s", "na", "nb", "nc", "no", "nd", "ni", "ne", "nf", "nl", "nn", "nx",
"py", "nt", "nv", "ow", "w", "mb", "mf", "mh", "mi", "mo", "sa", "sb", "sc", "dl", "sd",
"s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il",
]
def allow_class(_tag, name, value):
return name == "class" and value in ALLOWED_CSS
def allow_a(_tag, name, value):
return name in ["href", "title", "data-username"] or (name == "class" and value == "header-anchor")
ALLOWED_ATTRIBUTES = {
"h1": ["id"],
"h2": ["id"],
"h3": ["id"],
"h4": ["id"],
"a": allow_a,
"img": ["src", "title", "alt"],
"code": allow_class,
"div": allow_class,
"span": allow_class,
"table": ["id"],
}
ALLOWED_PROTOCOLS = {"http", "https", "mailto"}
def linker_callback(attrs, new=False):
if new:
text = attrs.get("_text")
if not (text.startswith("http://") or text.startswith("https://")):
return None
return attrs
def clean_html(html: str):
cleaner = Cleaner(
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
filters=[partial(LinkifyFilter,
callbacks=[linker_callback] + DEFAULT_CALLBACKS,
skip_tags={"pre", "code"})])
return cleaner.clean(html)

109
app/markdown/mention.py Normal file
View File

@@ -0,0 +1,109 @@
# ContentDB
# Copyright (C) rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import re
from flask import url_for
from markdown_it import MarkdownIt
from markdown_it.token import Token
from markdown_it.rules_core.state_core import StateCore
from typing import Sequence, List
def render_user_mention(self, tokens: Sequence[Token], idx, options, env):
token = tokens[idx]
username = token.content
url = url_for("users.profile", username=username)
return f"<a href=\"{url}\" data-username=\"{username}\">@{username}</a>"
def render_package_mention(self, tokens: Sequence[Token], idx, options, env):
token = tokens[idx]
username = token.content
name = token.attrs["name"]
url = url_for("packages.view", author=username, name=name)
return f"<a href=\"{url}\">@{username}/{name}</a>"
def parse_mentions(state: StateCore):
for block_token in state.tokens:
if block_token.type != "inline" or block_token.children is None:
continue
link_depth = 0
html_link_depth = 0
children = []
for token in block_token.children:
if token.type == "link_open":
link_depth += 1
elif token.type == "link_close":
link_depth -= 1
elif token.type == "html_inline":
# is link open / close?
pass
if link_depth > 0 or html_link_depth > 0 or token.type != "text":
children.append(token)
else:
children.extend(split_tokens(token, state))
block_token.children = children
RE_PARTS = dict(
USER=r"[A-Za-z0-9._-]*\b",
NAME=r"[A-Za-z0-9_]+\b"
)
MENTION_RE = r"(@({USER})(?:\/({NAME}))?)".format(**RE_PARTS)
def split_tokens(token: Token, state: StateCore) -> List[Token]:
tokens = []
content = token.content
pos = 0
for match in re.finditer(MENTION_RE, content):
username = match.group(2)
package_name = match.group(3)
(start, end) = match.span(0)
if start > pos:
token_text = Token("text", "", 0)
token_text.content = content[pos:start]
token_text.level = token.level
tokens.append(token_text)
mention = Token("package_mention" if package_name else "user_mention", "", 0)
mention.content = username
mention.attrSet("name", package_name)
mention.level = token.level
tokens.append(mention)
pos = end
if pos < len(content):
token_text = Token("text", "", 0)
token_text.content = content[pos:]
token_text.level = token.level
tokens.append(token_text)
return tokens
def init_mention(md: MarkdownIt):
md.add_render_rule("user_mention", render_user_mention, "html")
md.add_render_rule("package_mention", render_package_mention, "html")
md.core.ruler.after("inline", "mention", parse_mentions)

View File

@@ -13,8 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask_babel import LazyString
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_searchable import make_searchable
@@ -125,13 +124,115 @@ class AuditLogEntry(db.Model):
raise Exception("Unknown permission given to AuditLogEntry.check_perm()")
if perm == Permission.VIEW_AUDIT_DESCRIPTION:
return user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
return (self.package and user in self.package.maintainers) or user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
else:
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
class ReportCategory(enum.Enum):
ACCOUNT_DELETION = "account_deletion"
COPYRIGHT = "copyright"
USER_CONDUCT = "user_conduct"
SPAM = "spam"
ILLEGAL_HARMFUL = "illegal_harmful"
REVIEW = "review"
APPEAL = "appeal"
OTHER = "other"
def __str__(self):
return self.name
@property
def title(self) -> LazyString:
if self == ReportCategory.ACCOUNT_DELETION:
return lazy_gettext("Account deletion")
elif self == ReportCategory.COPYRIGHT:
return lazy_gettext("Copyright infringement / DMCA")
elif self == ReportCategory.USER_CONDUCT:
return lazy_gettext("User behaviour, bullying, or abuse")
elif self == ReportCategory.SPAM:
return lazy_gettext("Spam")
elif self == ReportCategory.ILLEGAL_HARMFUL:
return lazy_gettext("Illegal or harmful content")
elif self == ReportCategory.REVIEW:
return lazy_gettext("Outdated/invalid review")
elif self == ReportCategory.APPEAL:
return lazy_gettext("Appeal")
elif self == ReportCategory.OTHER:
return lazy_gettext("Other")
else:
raise Exception("Unknown report category")
@classmethod
def get(cls, name):
try:
return ReportCategory[name.upper()]
except KeyError:
return None
@classmethod
def choices(cls, with_none):
ret = [(choice, choice.title) for choice in cls]
if with_none:
ret.insert(0, (None, ""))
return ret
@classmethod
def coerce(cls, item):
if item is None or (isinstance(item, str) and item.upper() == "NONE"):
return None
return item if type(item) == ReportCategory else ReportCategory[item.upper()]
class Report(db.Model):
id = db.Column(db.String(24), primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
user = db.relationship("User", foreign_keys=[user_id], back_populates="reports")
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True)
thread = db.relationship("Thread", foreign_keys=[thread_id])
category = db.Column(db.Enum(ReportCategory), nullable=False)
url = db.Column(db.String, nullable=True)
title = db.Column(db.Unicode(300), nullable=False)
message = db.Column(db.UnicodeText, nullable=False)
is_resolved = db.Column(db.Boolean, nullable=False, default=False)
attachments = db.relationship("ReportAttachment", back_populates="report", lazy="dynamic", cascade="all, delete, delete-orphan")
def check_perm(self, user, perm):
if type(perm) == str:
perm = Permission[perm]
elif type(perm) != Permission:
raise Exception("Unknown permission given to Report.check_perm()")
if not user.is_authenticated:
return False
if perm == Permission.SEE_REPORT:
return user.rank.at_least(UserRank.EDITOR)
else:
raise Exception("Permission {} is not related to reports".format(perm.name))
class ReportAttachment(db.Model):
id = db.Column(db.Integer, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
report_id = db.Column(db.String(24), db.ForeignKey("report.id"), nullable=False)
report = db.relationship("Report", foreign_keys=[report_id], back_populates="attachments")
url = db.Column(db.String(100), nullable=False)
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
"minetest.net", "dropboxusercontent.com", "4shared.com",
"minetest.net", "luanti.org", "dropboxusercontent.com", "4shared.com",
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
"imageshack.com", "imgur.com"]
@@ -158,7 +259,7 @@ class ForumTopic(db.Model):
@property
def url(self):
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.topic_id)
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.topic_id)
def get_repo_url(self):
if self.link is None:

View File

@@ -457,7 +457,7 @@ class Package(db.Model):
if self.forums is None:
return None
return "https://forum.minetest.net/viewtopic.php?t=" + str(self.forums)
return "https://forum.luanti.org/viewtopic.php?t=" + str(self.forums)
enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True)
@@ -679,6 +679,7 @@ class Package(db.Model):
"website": self.website,
"issue_tracker": self.issueTracker,
"forums": self.forums,
"forum_url": self.forums_url,
"video_url": self.video_url,
"video_thumbnail_url": self.get_video_thumbnail_url(True),
"donate_url": self.donate_url_actual,
@@ -811,7 +812,7 @@ class Package(db.Model):
elif perm == Permission.APPROVE_SCREENSHOT:
return (is_maintainer or is_approver) and \
user.rank.at_least(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
user.rank.at_least(UserRank.TRUSTED_MEMBER if self.approved else UserRank.NEW_MEMBER)
elif perm == Permission.EDIT_MAINTAINERS or perm == Permission.DELETE_PACKAGE:
return is_owner or user.rank.at_least(UserRank.EDITOR)
@@ -1042,7 +1043,7 @@ class Tag(db.Model):
}
class MinetestRelease(db.Model):
class LuantiRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
protocol = db.Column(db.Integer, nullable=False, default=0)
@@ -1066,12 +1067,11 @@ class MinetestRelease(db.Model):
}
@classmethod
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["MinetestRelease"]:
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["LuantiRelease"]:
if version:
parts = version.strip().split(".")
if len(parts) >= 2:
major_minor = parts[0] + "." + parts[1]
query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
query = LuantiRelease.query.filter(func.replace(LuantiRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
if protocol_num:
query = query.filter_by(protocol=protocol_num)
@@ -1081,9 +1081,9 @@ class MinetestRelease(db.Model):
if protocol_num:
# Find the closest matching release
return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol),
db.desc(MinetestRelease.id)) \
.filter(MinetestRelease.protocol <= protocol_num).first()
return LuantiRelease.query.order_by(db.desc(LuantiRelease.protocol),
db.desc(LuantiRelease.id)) \
.filter(LuantiRelease.protocol <= protocol_num).first()
return None
@@ -1103,6 +1103,7 @@ class PackageRelease(db.Model):
commit_hash = db.Column(db.String(41), nullable=True, default=None)
downloads = db.Column(db.Integer, nullable=False, default=0)
release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
@property
def summary(self) -> str:
@@ -1113,11 +1114,11 @@ class PackageRelease(db.Model):
return self.release_notes.split("\n")[0]
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
min_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("LuantiRelease", foreign_keys=[min_rel_id])
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
max_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
max_rel = db.relationship("LuantiRelease", foreign_keys=[max_rel_id])
# If the release is approved, then the task_id must be null and the url must be present
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
@@ -1126,14 +1127,14 @@ class PackageRelease(db.Model):
def file_path(self):
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
@property
def file_size_bytes(self):
def calculate_file_size_bytes(self):
path = self.file_path
if not os.path.isfile(path):
return 0
self.file_size_bytes = 0
return
file_stats = os.stat(path)
return file_stats.st_size
self.file_size_bytes = file_stats.st_size
@property
def file_size(self):
@@ -1263,6 +1264,8 @@ class PackageScreenshot(db.Model):
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
def is_very_small(self):
return self.width < 720 or self.height < 405
@@ -1276,14 +1279,14 @@ class PackageScreenshot(db.Model):
def file_path(self):
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
@property
def file_size_bytes(self):
def calculate_file_size_bytes(self):
path = self.file_path
if not os.path.isfile(path):
return 0
self.file_size_bytes = 0
return
file_stats = os.stat(path)
return file_stats.st_size
self.file_size_bytes = file_stats.st_size
@property
def file_size(self):
@@ -1368,6 +1371,8 @@ class PackageUpdateConfig(db.Model):
# Set to now when an outdated notification is sent. Set to None when a release is created
outdated_at = db.Column(db.DateTime, nullable=True, default=None)
last_checked_at = db.Column(db.DateTime, nullable=True, default=None)
trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT)
ref = db.Column(db.String(41), nullable=True, default=None)
@@ -1438,7 +1443,7 @@ class PackageDailyStats(db.Model):
reason_update = db.Column(db.Integer, nullable=False, default=0)
@staticmethod
def update(package: Package, is_minetest: bool, reason: str):
def update(package: Package, is_luanti: bool, reason: str):
date = datetime.datetime.utcnow().date()
to_update = dict()
@@ -1446,7 +1451,7 @@ class PackageDailyStats(db.Model):
"package_id": package.id, "date": date
}
field_platform = "platform_minetest" if is_minetest else "platform_other"
field_platform = "platform_minetest" if is_luanti else "platform_other"
to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1
kwargs[field_platform] = 1

View File

@@ -57,6 +57,8 @@ class Thread(db.Model):
watchers = db.relationship("User", secondary=watchers, backref="watching")
report = db.relationship("Report", foreign_keys="Report.thread_id", back_populates="thread", lazy="dynamic")
first_reply = db.relationship("ThreadReply", uselist=False, foreign_keys="ThreadReply.thread_id",
lazy=True, order_by=db.asc("id"), viewonly=True,
primaryjoin="Thread.id==ThreadReply.thread_id")

View File

@@ -96,6 +96,7 @@ class Permission(enum.Enum):
CHANGE_USERNAMES = "CHANGE_USERNAMES"
CHANGE_RANK = "CHANGE_RANK"
CHANGE_EMAIL = "CHANGE_EMAIL"
LINK_TO_WEBSITE = "LINK_TO_WEBSITE"
SEE_THREAD = "SEE_THREAD"
CREATE_THREAD = "CREATE_THREAD"
COMMENT_THREAD = "COMMENT_THREAD"
@@ -114,6 +115,7 @@ class Permission(enum.Enum):
EDIT_COLLECTION = "EDIT_COLLECTION"
VIEW_COLLECTION = "VIEW_COLLECTION"
CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT"
SEE_REPORT = "SEE_REPORT"
# Only return true if the permission is valid for *all* contexts
# See Package.check_perm for package-specific contexts
@@ -210,6 +212,7 @@ class User(db.Model, UserMixin):
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
collections = db.relationship("Collection", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.asc("title"))
clients = db.relationship("OAuthClient", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
reports = db.relationship("Report", back_populates="user", lazy="dynamic", cascade="all")
ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
@@ -260,7 +263,7 @@ class User(db.Model, UserMixin):
return "/static/bot_avatar.png"
else:
from app.utils.gravatar import get_gravatar
return get_gravatar(self.email or f"{self.username}@content.minetest.net")
return get_gravatar(self.email or f"{self.username}@content.luanti.org")
def check_perm(self, user, perm):
if not user.is_authenticated:
@@ -287,6 +290,8 @@ class User(db.Model, UserMixin):
return user.rank.at_least(UserRank.NEW_MEMBER)
else:
return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank)
elif perm == Permission.LINK_TO_WEBSITE:
return user.rank.at_least(UserRank.MEMBER)
else:
raise Exception("Permission {} is not related to users".format(perm.name))

119
app/public/funding.json Normal file
View File

@@ -0,0 +1,119 @@
{
"version": "v1.0.0",
"entity": {
"type": "organisation",
"role": "maintainer",
"name": "Luanti",
"email": "rw@rubenwardy.com",
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
"webpageUrl": {
"url": "https://www.luanti.org"
}
},
"projects": [
{
"guid": "luanti",
"name": "Luanti",
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
"webpageUrl": {
"url": "https://www.luanti.org"
},
"repositoryUrl": {
"url": "https://github.com/luanti-org/luanti"
},
"licenses": [
"spdx:LGPL-2.1",
"spdx:CC-BY-SA-3.0",
"spdx:MIT",
"spdx:Apache-2.0"
],
"tags": [
"lua",
"voxel",
"game"
]
},
{
"guid": "contentdb",
"name": "Luanti ContentDB",
"description": "A content database for Luanti mods, games, and more.",
"webpageUrl": {
"url": "https://content.luanti.org/about/"
},
"repositoryUrl": {
"url": "https://github.com/luanti-org/contentdb"
},
"licenses": [
"spdx:AGPL-3.0",
"spdx:CC-BY-SA-4.0"
],
"tags": [
"python",
"flask",
"luanti",
"minetest"
]
}
],
"funding": {
"channels": [
{
"guid": "open-collective",
"type": "other",
"address": "https://opencollective.com/luanti",
"description": "Recurring and one-time donations to Luanti"
}
],
"plans": [
{
"guid": "oc-eur-backer",
"status": "active",
"name": "Luanti backer",
"description": "Become a backer for €5 per month and help Luanti development",
"amount": 5,
"currency": "EUR",
"frequency": "monthly",
"channels": [
"open-collective"
]
},
{
"guid": "oc-eur-supporter",
"status": "active",
"name": "Luanti supporter",
"description": "Become a supporter for €5 per month and help Luanti development",
"amount": 100,
"currency": "EUR",
"frequency": "monthly",
"channels": [
"open-collective"
]
},
{
"guid": "oc-eur-custom",
"status": "active",
"name": "Luanti custom one-off",
"description": "You may donate any amount you're comfortable with",
"amount": 0,
"currency": "EUR",
"frequency": "one-time",
"channels": [
"open-collective"
]
},
{
"guid": "fosdem",
"status": "active",
"name": "FOSDEM",
"description": "It costs us €3000 to attend FOSDEM",
"amount": 3000,
"currency": "EUR",
"frequency": "one-time",
"channels": [
"open-collective"
]
}
],
"history": []
}
}

View File

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

View File

@@ -26,7 +26,7 @@ window.addEventListener("load", () => {
try {
const pasteData = e.clipboardData.getData('text');
const url = new URL(pasteData);
if (url.hostname === "forum.minetest.net") {
if (url.hostname === "forum.luanti.org") {
forumsField.value = url.searchParams.get("t");
e.preventDefault();
}
@@ -37,7 +37,7 @@ window.addEventListener("load", () => {
const openForums = document.getElementById("forums-button");
openForums.addEventListener("click", () => {
window.open("https://forum.minetest.net/viewtopic.php?t=" + forumsField.value, "_blank");
window.open("https://forum.luanti.org/viewtopic.php?t=" + forumsField.value, "_blank");
});
function setupHints(id, hints) {
@@ -68,8 +68,9 @@ window.addEventListener("load", () => {
}
setupHints("short_desc", {
"short_desc_mods": (val) => val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0,
"short_desc_mods": (val) => val.indexOf("luanti") >= 0 || val.indexOf("minetest") >= 0 ||
val.indexOf("mod") >= 0 || val.indexOf("modpack") >= 0 ||
val.indexOf("mod pack") >= 0,
});
setupHints("desc", {
@@ -85,7 +86,8 @@ window.addEventListener("load", () => {
"desc_page_topic": (val) => {
const topicId = document.getElementById("forums").value;
const r = new RegExp(`forum\\.minetest\\.net\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
return topicId && r.test(val);
const r2 = new RegExp(`forum\\.luanti\\.org\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
return topicId && (r.test(val) || r2.test(val));
},
"desc_page_repo": (val) => {
const repoUrl = document.getElementById("repo").value.replace(".git", "");

View File

@@ -22,7 +22,7 @@ function sleep(interval) {
}
async function pollTask(poll_url, disableTimeout) {
async function pollTask(poll_url, disableTimeout, onProgress) {
let tries = 0;
while (true) {
@@ -42,6 +42,10 @@ async function pollTask(poll_url, disableTimeout) {
console.error(e);
}
if (res && res.status) {
onProgress?.(res);
}
if (res && res.status === "SUCCESS") {
console.log("Got result")
return res.result;
@@ -62,3 +66,41 @@ async function performTask(url) {
throw "Start task didn't return string!";
}
}
window.addEventListener("load", () => {
const taskId = document.querySelector("[data-task-id]")?.getAttribute("data-task-id");
if (taskId) {
const progress = document.getElementById("progress");
function onProgress(res) {
let status = res.status.toLowerCase();
if (status === "progress") {
progress.classList.remove("d-none");
const bar = progress.children[0];
const {current, total, running} = res.result;
const perc = Math.min(Math.max(100 * current / total, 0), 100);
bar.style.width = `${perc}%`;
bar.setAttribute("aria-valuenow", current);
bar.setAttribute("aria-valuemax", total);
const packages = (running ?? []).map(x => `${x.author}/${x.name}`).join(", ");
document.getElementById("status").textContent = `Status: in progress (${current} / ${total})\n\n${packages}`;
} else {
progress.classList.add("d-none");
if (status === "pending") {
status = "pending or unknown";
}
document.getElementById("status").textContent = `Status: ${status}`;
}
}
pollTask(`/tasks/${taskId}/`, true, onProgress)
.then(function() { location.reload() })
.catch(function(e) {
console.error(e);
location.reload();
});
}
});

View File

@@ -3,7 +3,7 @@
<ShortName>ContentDB</ShortName>
<LongName>ContentDB</LongName>
<InputEncoding>UTF-8</InputEncoding>
<Description>Search mods, games, and textures for Minetest.</Description>
<Tags>Minetest Mod Game Subgame Search</Tags>
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
<Description>Search mods, games, and textures for Luanti.</Description>
<Tags>Luanti Minetest Mod Game Subgame Search</Tags>
<Url type="text/html" method="get" template="https://content.luanti.org/packages?q={searchTerms}"/>
</OpenSearchDescription>

View File

@@ -22,7 +22,7 @@ from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import func
from sqlalchemy_searchable import search
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \
from .models import db, PackageType, Package, ForumTopic, License, LuantiRelease, PackageRelease, User, Tag, \
ContentWarning, PackageState, PackageDevState
from .utils import is_yes, get_int_or_abort
@@ -49,7 +49,7 @@ class QueryBuilder:
hide_wip: bool
hide_nonfree: bool
show_added: bool
version: Optional[MinetestRelease]
version: Optional[LuantiRelease]
has_lang: Optional[str]
@property
@@ -163,12 +163,12 @@ class QueryBuilder:
self.author = args.get("author")
protocol_version = get_int_or_abort(args.get("protocol_version"))
minetest_version = args.get("engine_version")
if minetest_version == "":
minetest_version = None
engine_version = args.get("engine_version")
if engine_version == "":
engine_version = None
if protocol_version or minetest_version:
self.version = MinetestRelease.get(minetest_version, protocol_version)
if protocol_version or engine_version:
self.version = LuantiRelease.get(engine_version, protocol_version)
else:
self.version = None

View File

@@ -283,3 +283,7 @@ blockquote {
.form-group {
margin-bottom: 1rem !important;
}
input[name="first_name"] {
display: none;
}

View File

@@ -51,6 +51,26 @@ h3 {
letter-spacing: .05em
}
h1, h2, h3, h4, h5, h6 {
&::after {
display: block;
content: "";
clear: both;
}
.header-anchor {
transition: opacity 0.15s ease-in-out;
opacity: 0.25;
margin: 0 0 0 0.25em;
font-size: 60%;
}
&:hover .header-anchor {
opacity: 0.9;
}
}
.badge-notify {
background:yellow; /* #00bc8c;*/
color: black;

View File

@@ -92,3 +92,12 @@
max-height: 1em;
filter: none !important;
}
.release-notes-body {
max-height: 20em;
overflow: hidden auto;
> *:first-child {
margin-top: 0;
}
}

View File

@@ -64,7 +64,7 @@ class FlaskCelery(Celery):
def make_celery(app):
celery = FlaskCelery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
broker=app.config['CELERY_BROKER_URL'])
broker=app.config['CELERY_BROKER_URL'], task_track_started=True)
celery.init_app(app)
return celery

View File

@@ -57,7 +57,7 @@ def _get_or_create_user(forums_username: str, cache: Optional[dict] = None) -> O
def check_forum_account(forums_username, force_replace_pic=False):
print("### Checking " + forums_username, file=sys.stderr)
try:
profile = get_profile("https://forum.minetest.net", forums_username)
profile = get_profile("https://forum.luanti.org", forums_username)
except OSError as e:
print(e, file=sys.stderr)
return
@@ -88,13 +88,13 @@ def check_forum_account(forums_username, force_replace_pic=False):
db.session.commit()
if pic:
pic = urljoin("https://forum.minetest.net/", pic)
pic = urljoin("https://forum.luanti.org/", pic)
print(f"####### Picture: {pic}", file=sys.stderr)
print(f"####### User pp {user.profile_pic}", file=sys.stderr)
pic_needs_replacing = user.profile_pic is None or user.profile_pic == "" or \
user.profile_pic.startswith("https://forum.minetest.net") or force_replace_pic
if pic_needs_replacing and pic.startswith("https://forum.minetest.net"):
user.profile_pic.startswith("https://forum.luanti.org") or force_replace_pic
if pic_needs_replacing and pic.startswith("https://forum.luanti.org"):
print(f"####### Queueing", file=sys.stderr)
set_profile_picture_from_url.delay(user.username, pic)

View File

@@ -20,7 +20,7 @@ import os
import shutil
import sys
from json import JSONDecodeError
from zipfile import ZipFile
from zipfile import ZipFile, BadZipFile
import gitdb
from flask import url_for
@@ -31,13 +31,13 @@ from sqlalchemy import and_
from sqlalchemy.dialects.postgresql import insert
from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \
MinetestRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
LuantiRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
PackageGameSupport, PackageTranslation, Language
from app.tasks import celery, TaskError
from app.utils import random_string, post_bot_message, add_system_notification, add_system_audit_log, \
get_games_from_list, add_audit_log
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir, get_release_notes
from .minetestcheck import build_tree, MinetestCheckError, ContentType, PackageTreeNode
from .luanticheck import build_tree, LuantiCheckError, ContentType, PackageTreeNode
from .webhooktasks import post_discord_webhook
from app import app
from app.logic.LogicError import LogicError
@@ -51,7 +51,7 @@ def get_meta(urlstr, author):
with clone_repo(urlstr, recursive=True) as repo:
try:
tree = build_tree(repo.working_tree_dir, author=author, repo=urlstr)
except MinetestCheckError as err:
except LuantiCheckError as err:
raise TaskError(str(err))
result = {"name": tree.name, "type": tree.type.name}
@@ -71,8 +71,6 @@ def get_meta(urlstr, author):
data = json.loads(f.read())
for key, value in data.items():
result[key] = value
except LogicError as e:
raise TaskError(e.message)
except JSONDecodeError as e:
raise TaskError("Whilst reading .cdb.json: " + str(e))
except IOError:
@@ -115,7 +113,9 @@ def post_release_check_update(self, release: PackageRelease, path):
author=release.package.author.username, name=release.package.name)
if tree.name is not None and release.package.name != tree.name and tree.type == ContentType.MOD:
raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}")
raise LuantiCheckError(f"Package name ({release.package.name}) does not match the name of the content in "
f"the release ({tree.name}). Either change the package name on ContentDB or the "
f"name in the .conf of the content. Then make a new release")
cache = {}
def get_meta_packages(names):
@@ -124,6 +124,9 @@ def post_release_check_update(self, release: PackageRelease, path):
provides = tree.get_mod_names()
package = release.package
if not package.approved:
tree.check_for_legacy_files()
old_provided_names = set([x.name for x in package.provides])
package.provides.clear()
@@ -164,10 +167,10 @@ def post_release_check_update(self, release: PackageRelease, path):
# Raise error on unresolved game dependencies
if package.type == PackageType.GAME and len(depends) > 0:
deps = ", ".join(depends)
raise MinetestCheckError("Game has unresolved hard dependencies: " + deps)
raise LuantiCheckError("Game has unresolved hard dependencies: " + deps)
if package.state != PackageState.APPROVED and tree.find_license_file() is None:
raise MinetestCheckError(
raise LuantiCheckError(
"You need to add a LICENSE.txt/.md or COPYING file to your package. See the 'Copyright Guide' for more info")
# Add dependencies
@@ -182,17 +185,17 @@ def post_release_check_update(self, release: PackageRelease, path):
# Update min/max
if tree.meta.get("min_minetest_version"):
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
release.min_rel = LuantiRelease.get(tree.meta["min_minetest_version"], None)
if tree.meta.get("max_minetest_version"):
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
release.max_rel = LuantiRelease.get(tree.meta["max_minetest_version"], None)
try:
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:
data = json.loads(f.read())
do_edit_package(package.author, package, False, False, data, "Post release hook")
except LogicError as e:
raise TaskError(e.message)
raise TaskError("Whilst applying .cdb.json: " + e.message)
except JSONDecodeError as e:
raise TaskError("Whilst reading .cdb.json: " + str(e))
except IOError:
@@ -231,7 +234,7 @@ def post_release_check_update(self, release: PackageRelease, path):
return tree
except (MinetestCheckError, TaskError, LogicError) as err:
except (LuantiCheckError, TaskError, LogicError) as err:
db.session.rollback()
error_message = err.value if hasattr(err, "value") else str(err)
@@ -268,11 +271,8 @@ def update_translations(package: Package, tree: PackageTreeNode):
)
conn.execute(stmt)
raw_translations = tree.get_translations(tree.get("textdomain", tree.name))
raw_translations = tree.get_translations(tree.get("textdomain", tree.name), allowed_languages=allowed_languages)
for raw_translation in raw_translations:
if raw_translation.language not in allowed_languages:
continue
to_update = {
"title": raw_translation.entries.get(tree.get("title", package.title)),
"short_desc": raw_translation.entries.get(tree.get("description", package.short_desc)),
@@ -306,13 +306,16 @@ def _check_zip_file(temp_dir: str, zf: ZipFile) -> bool:
def _safe_extract_zip(temp_dir: str, archive_path: str) -> bool:
with ZipFile(archive_path, 'r') as zf:
if not _check_zip_file(temp_dir, zf):
return False
try:
with ZipFile(archive_path, 'r') as zf:
if not _check_zip_file(temp_dir, zf):
return False
# Extract all
for member in zf.infolist():
zf.extract(member, temp_dir)
# Extract all
for member in zf.infolist():
zf.extract(member, temp_dir)
except BadZipFile as e:
raise TaskError(str(e))
return True
@@ -334,6 +337,7 @@ def check_zip_release(self, id, path):
post_release_check_update(self, release, temp)
release.task_id = None
release.calculate_file_size_bytes()
release.approve(release.package.author)
db.session.commit()
@@ -342,16 +346,15 @@ def check_zip_release(self, id, path):
def check_all_zip_files():
result = []
with get_temp_dir() as temp:
releases = PackageRelease.query.all()
for release in releases:
with ZipFile(release.file_path, 'r') as zf:
if not _check_zip_file(temp, zf):
print(f"Unsafe zip file for {release.package.get_id} at {release.file_path}", file=sys.stderr)
result.append({
"package": release.package.get_id(),
"file": release.file_path,
})
releases = PackageRelease.query.all()
for release in releases:
with ZipFile(release.file_path, 'r') as zf:
if not _check_zip_file("/tmp/example", zf):
print(f"Unsafe zip file for {release.package.get_id()} at {release.file_path}", file=sys.stderr)
result.append({
"package": release.package.get_id(),
"file": release.file_path,
})
return json.dumps(result)
@@ -376,7 +379,7 @@ def import_languages(self, id, path):
strict=False)
update_translations(release.package, tree)
db.session.commit()
except (MinetestCheckError, TaskError, LogicError) as err:
except (LuantiCheckError, TaskError, LogicError) as err:
db.session.rollback()
task_url = url_for('tasks.check', id=self.request.id)
@@ -414,6 +417,7 @@ def make_vcs_release(self, id, branch):
release.url = "/uploads/" + filename
release.task_id = None
release.calculate_file_size_bytes()
release.approve(release.package.author)
db.session.commit()
@@ -476,13 +480,11 @@ def check_update_config_impl(package):
if config.last_commit == commit:
if tag and config.last_tag != tag:
config.last_tag = tag
db.session.commit()
return
if not config.last_commit:
config.last_commit = commit
config.last_tag = tag
db.session.commit()
return
if package.releases.filter_by(commit_hash=commit).count() > 0:
@@ -501,8 +503,6 @@ def check_update_config_impl(package):
msg = "Created release {} (Git Update Detection)".format(rel.title)
add_system_audit_log(AuditSeverity.NORMAL, msg, package.get_url("packages.view"), package)
db.session.commit()
make_vcs_release.apply_async((rel.id, commit), task_id=rel.task_id)
elif config.outdated_at is None:
@@ -529,10 +529,9 @@ def check_update_config_impl(package):
config.last_commit = commit
config.last_tag = tag
db.session.commit()
@celery.task(bind=True)
@celery.task(bind=True, rate_limit="60/m")
def check_update_config(self, package_id):
package: Package = Package.query.get(package_id)
if package is None:
@@ -543,6 +542,9 @@ def check_update_config(self, package_id):
err = None
try:
check_update_config_impl(package)
package.update_config.last_checked_at = datetime.datetime.now()
db.session.commit()
except GitCommandError as e:
# This is needed to stop the backtrace being weird
err = e.stderr

View File

@@ -17,7 +17,7 @@
from enum import Enum
class MinetestCheckError(Exception):
class LuantiCheckError(Exception):
def __init__(self, value):
self.value = value
@@ -43,14 +43,14 @@ class ContentType(Enum):
if self == ContentType.MOD:
if not other.is_mod_like():
raise MinetestCheckError("Expected a mod or modpack, found " + other.value)
raise LuantiCheckError("Expected a mod or modpack, found " + other.value)
elif self == ContentType.TXP:
if other != ContentType.UNKNOWN and other != ContentType.TXP:
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
raise LuantiCheckError("expected a " + self.value + ", found a " + other.value)
elif other != self:
raise MinetestCheckError("Expected a " + self.value + ", found a " + other.value)
raise LuantiCheckError("Expected a " + self.value + ", found a " + other.value)
from .tree import PackageTreeNode, get_base_dir

View File

@@ -20,12 +20,12 @@ import re
import glob
from typing import Optional
from . import MinetestCheckError, ContentType
from . import LuantiCheckError, ContentType
from .config import parse_conf
from .translation import Translation, parse_tr
basenamePattern = re.compile("^([a-z0-9_]+)$")
licensePattern = re.compile("^(licen[sc]e|copying)(.[^/\n]+)?$", re.IGNORECASE)
licensePattern = re.compile("^licen[sc]e[^/.]*(\.(txt|md))?$", re.IGNORECASE)
DISALLOWED_NAMES = {
"core", "minetest", "group", "table", "string", "lua", "luajit", "assert", "debug",
@@ -73,10 +73,10 @@ def check_name_list(key: str, value: list[str], relative: str, allow_star: bool
if dep == "*" and allow_star:
continue
elif " " in dep:
raise MinetestCheckError(
raise LuantiCheckError(
f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?")
else:
raise MinetestCheckError(
raise LuantiCheckError(
f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.")
@@ -90,6 +90,8 @@ class PackageTreeNode:
children: list
type: ContentType
strict: bool
has_legacy_depends: bool
has_legacy_description: bool
def __init__(self, base_dir: str, relative: str,
author: Optional[str] = None,
@@ -103,6 +105,8 @@ class PackageTreeNode:
self.meta = {}
self.children = []
self.strict = strict
self.has_legacy_depends = False
self.has_legacy_description = False
# Detect type
self.type = detect_type(base_dir)
@@ -110,14 +114,14 @@ class PackageTreeNode:
if self.type == ContentType.GAME:
if not os.path.isdir(os.path.join(base_dir, "mods")):
raise MinetestCheckError("Game at {} does not have a mods/ folder".format(self.relative))
raise LuantiCheckError("Game at {} does not have a mods/ folder".format(self.relative))
self._add_children_from_mod_dir("mods")
elif self.type == ContentType.MOD:
if self.name and not basenamePattern.match(self.name):
raise MinetestCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
raise LuantiCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
if self.name and self.name in DISALLOWED_NAMES:
raise MinetestCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
raise LuantiCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
self._check_dir_casing(["textures", "media", "sounds", "models", "locale"])
elif self.type == ContentType.MODPACK:
@@ -135,7 +139,7 @@ class PackageTreeNode:
for dir in next(os.walk(self.baseDir))[1]:
lowercase = dir.lower()
if lowercase != dir and lowercase in dirs:
raise MinetestCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
raise LuantiCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
def get_readme_path(self):
for filename in os.listdir(self.baseDir):
@@ -169,12 +173,12 @@ class PackageTreeNode:
for key, value in conf.items():
result[key] = value
except SyntaxError as e:
raise MinetestCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
raise LuantiCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
except IOError:
pass
if self.strict and "release" in result:
raise MinetestCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
raise LuantiCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
# description.txt
if "description" not in result:
@@ -184,6 +188,11 @@ class PackageTreeNode:
except IOError:
pass
if os.path.isfile(self.baseDir + "/depends.txt"):
self.has_legacy_depends = True
if os.path.isfile(self.baseDir + "/description.txt"):
self.has_legacy_description = True
# Read dependencies
if "depends" in result or "optional_depends" in result:
result["depends"] = get_csv_line(result.get("depends"))
@@ -235,9 +244,12 @@ class PackageTreeNode:
# Calculate short description
if "description" in result:
desc = result["description"]
idx = desc.find(".") + 1
cutIdx = min(len(desc), 200 if idx < 5 else idx)
result["short_description"] = desc[:cutIdx]
if len(desc) > 200:
idx = desc.find(".") + 1
idx = min(len(desc), 200 if idx < 5 else idx)
result["short_description"] = desc[:idx]
else:
result["short_description"] = desc
if "name" in result:
self.name = result["name"]
@@ -257,11 +269,11 @@ class PackageTreeNode:
if not entry.startswith('.') and os.path.isdir(path):
child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict)
if not child.type.is_mod_like():
raise MinetestCheckError("Expecting mod or modpack, found {} at {} inside {}" \
raise LuantiCheckError("Expecting mod or modpack, found {} at {} inside {}" \
.format(child.type.value, child.relative, self.type.value))
if child.name is None:
raise MinetestCheckError("Missing base name for mod at {}".format(self.relative))
raise LuantiCheckError("Missing base name for mod at {}".format(self.relative))
self.children.append(child)
@@ -301,6 +313,16 @@ class PackageTreeNode:
def get(self, key: str, default=None):
return self.meta.get(key, default)
def check_for_legacy_files(self):
if self.has_legacy_depends:
raise LuantiCheckError("Found depends.txt at {}. Delete this file and use depends in mod.conf instead" \
.format(self.relative))
if self.has_legacy_description:
raise LuantiCheckError("Found description.txt at {}. Delete this file and use description in {} instead" \
.format(self.relative, self.get_meta_file_name()))
for child in self.children:
child.check_for_legacy_files()
def validate(self):
for child in self.children:
child.validate()
@@ -313,14 +335,19 @@ class PackageTreeNode:
return ret
def get_translations(self, textdomain: str) -> list[Translation]:
def get_translations(self, textdomain: str, allowed_languages: set[str]) -> list[Translation]:
ret = []
for name in glob.glob(f"{self.baseDir}/**/locale/{textdomain}.*.tr", recursive=True):
parts = os.path.basename(name).split(".")
lang = parts[-2]
if lang not in allowed_languages:
continue
try:
ret.append(parse_tr(name))
except SyntaxError as e:
relative_path = os.path.join(self.relative, os.path.relpath(name, self.baseDir))
raise MinetestCheckError(f"Syntax error whilst reading {relative_path}: {e}")
raise LuantiCheckError(f"Syntax error whilst reading {relative_path}: {e}")
return ret

View File

@@ -19,15 +19,16 @@ import random
import re
import sys
from time import sleep
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
from typing import Optional
import requests
import urllib3
from app import app
from sqlalchemy import or_, and_
from app.markdown import get_links, render_markdown
from app.models import Package, db, PackageState, AuditLogEntry, AuditSeverity
from app.models import db, Package, PackageState, PackageRelease, PackageScreenshot, AuditLogEntry, AuditSeverity
from app.tasks import celery, TaskError
from app.utils import post_bot_message, post_to_approval_thread, get_system_user, add_audit_log
@@ -44,7 +45,7 @@ def update_package_scores():
def desc_contains(desc: str, search_str: str):
if search_str.startswith("https://forum.minetest.net/viewtopic.php?%t="):
if search_str.startswith("https://forum.luanti.org/viewtopic.php?%t="):
reg = re.compile(search_str.replace(".", "\\.").replace("/", "\\/").replace("?", "\\?").replace("%", ".*"))
return reg.search(desc)
else:
@@ -57,7 +58,7 @@ def notify_about_git_forum_links():
.filter(Package.repo.is_not(None), Package.state == PackageState.APPROVED).all()]
for pair in db.session.query(Package, Package.forums) \
.filter(Package.forums.is_not(None), Package.state == PackageState.APPROVED).all():
package_links.append((pair[0], f"https://forum.minetest.net/viewtopic.php?%t={pair[1]}"))
package_links.append((pair[0], f"https://forum.luanti.org/viewtopic.php?%t={pair[1]}"))
clauses = [and_(Package.id != pair[0].id, Package.desc.ilike(f"%{pair[1]}%")) for pair in package_links]
packages = Package.query.filter(Package.desc != "", Package.desc.is_not(None), Package.state == PackageState.APPROVED, or_(*clauses)).all()
@@ -110,12 +111,15 @@ def clear_removed_packages(all_packages: bool):
def _url_exists(url: str) -> str:
try:
headers = {
"User-Agent": "Mozilla/5.0 (compatible; ContentDB link checker; +https://content.minetest.net/)",
"User-Agent": "Mozilla/5.0 (compatible; ContentDB link checker; +https://content.luanti.org/)",
}
with requests.get(url, stream=True, headers=headers, timeout=10) as response:
response.raise_for_status()
return ""
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
return ""
print(f" - [{e.response.status_code}] <{url}>", file=sys.stderr)
return str(e.response.status_code)
except requests.exceptions.ConnectionError:
@@ -125,6 +129,9 @@ def _url_exists(url: str) -> str:
def _check_for_dead_links(package: Package) -> dict[str, str]:
ignored_urls = set(app.config.get("LINK_CHECKER_IGNORED_URLS", ""))
base_url = package.get_url("packages.view", absolute=True)
links: set[Optional[str]] = {
package.repo,
package.website,
@@ -136,7 +143,7 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
}
if package.desc:
links.update(get_links(render_markdown(package.desc), package.get_url("packages.view", absolute=True)))
links.update(get_links(render_markdown(package.desc)))
print(f"Checking {package.title} ({len(links)} links) for broken links", file=sys.stderr)
@@ -146,11 +153,15 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
if link is None:
continue
url = urlparse(link)
abs_link = urljoin(base_url, link)
url = urlparse(abs_link)
if url.scheme != "http" and url.scheme != "https":
continue
res = _url_exists(link)
if url.hostname in ignored_urls:
continue
res = _url_exists(abs_link)
if res != "":
bad_urls[link] = res
@@ -180,7 +191,7 @@ def check_package_on_submit(package_id: int):
msg = _check_package(package)
if msg:
marked = f"Marked {package.title} as Changed Needed"
marked = f"Marked {package.title} as {PackageState.CHANGES_NEEDED.value}"
system_user = get_system_user()
post_to_approval_thread(package, system_user, marked, is_status_update=True, create_thread=True)
@@ -200,3 +211,34 @@ def check_package_for_broken_links(package_id: int):
if msg:
post_bot_message(package, "Broken links", msg)
db.session.commit()
@celery.task(bind=True)
def update_file_size_bytes(self):
releases = PackageRelease.query.filter_by(file_size_bytes=0).all()
screenshots = PackageScreenshot.query.filter_by(file_size_bytes=0).all()
total = len(releases) + len(screenshots)
self.update_state(state="PROGRESS", meta={
"current": 0,
"total": total,
})
for i, release in enumerate(releases):
release.calculate_file_size_bytes()
if i % 100 == 0:
self.update_state(state="PROGRESS", meta={
"current": i + 1,
"total": total,
})
for i, ss in enumerate(screenshots):
ss.calculate_file_size_bytes()
if i % 100 == 0:
self.update_state(state="PROGRESS", meta={
"current": i + len(releases) + 1,
"total": total,
})
db.session.commit()

View File

@@ -20,7 +20,7 @@ import os
import sys
from flask import url_for
from sqlalchemy import or_, and_
from sqlalchemy import or_, and_, not_, func
from app import app
from app.models import User, db, UserRank, ThreadReply, Package, NotificationType
@@ -149,3 +149,37 @@ def import_github_user_ids():
db.session.commit()
print(f"Updated {count} users", file=sys.stderr)
@celery.task()
def do_delete_likely_spammers():
query = (User.query.filter(
and_(
User.rank == UserRank.NEW_MEMBER,
or_(
func.replace(User.website_url, ".", "").regexp_match(
func.concat("https?://[^/]*", User.username, ".*")),
),
or_(
User.website_url.ilike("%bet%"),
User.website_url.ilike("%win%"),
User.website_url.ilike("%88%"),
User.website_url.ilike("%luck%"),
User.website_url.ilike("%sport%"),
User.website_url.ilike("%lottery%"),
User.website_url.ilike("%casino%"),
User.website_url.ilike("%vip%"),
User.website_url.ilike("%assignment%"),
),
~User.packages.any(),
~User.replies.any(),
~User.reports.any(),
not_(or_(
User.website_url.ilike("%.github.io%"),
User.website_url.ilike("%.neocities.org%"),
)),
)))
for user in query.all():
db.session.delete(user)
db.session.commit()

View File

@@ -25,10 +25,13 @@ from app.tasks import celery
@celery.task()
def post_discord_webhook(username: Optional[str], content: str, is_queue: bool, title: Optional[str] = None, description: Optional[str] = None, thumbnail: Optional[str] = None):
discord_url = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
if discord_url is None:
discord_urls = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
if discord_urls is None:
return
if isinstance(discord_urls, str):
discord_urls = [discord_urls]
json = {
"content": content[0:2000],
}
@@ -52,7 +55,8 @@ def post_discord_webhook(username: Optional[str], content: str, is_queue: bool,
json["embeds"] = [embed]
res = requests.post(discord_url, json=json, headers={"Accept": "application/json"})
if not res.ok:
raise Exception(f"Failed to submit Discord webhook {res.json}")
res.raise_for_status()
for url in discord_urls:
res = requests.post(url, json=json, headers={"Accept": "application/json"})
if not res.ok:
raise Exception(f"Failed to submit Discord webhook {res.json}")
res.raise_for_status()

View File

@@ -15,45 +15,70 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import subprocess
from subprocess import Popen, PIPE
from typing import Optional
import sys
from subprocess import Popen, PIPE, TimeoutExpired
from typing import Optional, List
from app.models import Package, PackageState, PackageRelease
from app.tasks import celery
@celery.task()
def search_in_releases(query: str, file_filter: str):
packages = list(Package.query.filter(Package.state == PackageState.APPROVED).all())
running = []
@celery.task(bind=True)
def search_in_releases(self, query: str, file_filter: str, types: List[str]):
pkg_query = Package.query.filter(Package.state == PackageState.APPROVED)
if len(types) > 0:
pkg_query = pkg_query.filter(Package.type.in_(types))
packages = list(pkg_query.all())
results = []
while len(packages) > 0 or len(running) > 0:
# Check running
for i in range(len(running) - 1, -1, -1):
package: Package = running[i][0]
handle: subprocess.Popen[str] = running[i][1]
total = len(packages)
self.update_state(state="PROGRESS", meta={"current": 0, "total": total})
while len(packages) > 0:
package = packages.pop()
release: Optional[PackageRelease] = package.get_download_release()
if release:
print(f"[Zipgrep] Checking {package.name}", file=sys.stderr)
self.update_state(state="PROGRESS", meta={
"current": total - len(packages),
"total": total,
"running": [package.as_key_dict()],
})
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
try:
handle.wait(timeout=45)
except TimeoutExpired:
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
handle.kill()
results.append({
"package": package.as_key_dict(),
"lines": "Error: timeout",
})
continue
exit_code = handle.poll()
if exit_code is None:
continue
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
handle.kill()
results.append({
"package": package.as_key_dict(),
"lines": "Error: timeout",
})
elif exit_code == 0:
print(f"[Zipgrep] Success for {package.name}", file=sys.stderr)
results.append({
"package": package.as_key_dict(),
"lines": handle.stdout.read(),
})
del running[i]
# Create new
while len(running) < 1 and len(packages) > 0:
package = packages.pop()
release: Optional[PackageRelease] = package.get_download_release()
if release:
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
running.append([package, handle])
if len(running) > 0:
running[0][1].wait()
elif exit_code != 1:
print(f"[Zipgrep] Error {exit_code} for {package.name}", file=sys.stderr)
results.append({
"package": package.as_key_dict(),
"lines": f"Error: exit {exit_code}",
})
return {
"query": query,

View File

@@ -23,10 +23,10 @@ from flask_login import current_user
from markupsafe import Markup
from . import app, utils
from .markdown import get_headings
from app.markdown import get_headings
from .models import Permission, Package, PackageState, PackageRelease
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current
from .utils.minetest_hypertext import normalize_whitespace as do_normalize_whitespace
from .utils.luanti_hypertext import normalize_whitespace as do_normalize_whitespace
@app.context_processor

View File

@@ -7,6 +7,14 @@ Audit Log
{% block content %}
<h1>Audit Log</h1>
{% from "macros/forms.html" import render_field, render_submit_field %}
<form method="GET" action="">
{{ render_field(form.username) }}
{{ render_field(form.q) }}
{{ render_field(form.url) }}
{{ render_submit_field(form.submit) }}
</form>
{% from "macros/pagination.html" import render_pagination %}
{% from "macros/audit_log.html" import render_audit_log %}

View File

@@ -13,14 +13,20 @@
<div class="list-group">
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">
<i class="fas fa-users me-2"></i>
User list
{{ _("User list") }}
</a>
{% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
{% if current_user.rank.at_least(current_user.rank.APPROVER) %}
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.audit') }}">
<i class="fas fa-user-clock me-2"></i>
{{ _("Audit Log") }}
</a>
{% endif %}
{% if current_user.rank.at_least(current_user.rank.EDITOR) %}
<a class="list-group-item list-group-item-action" href="{{ url_for('report.list_all') }}">
<i class="fas fa-user-clock me-2"></i>
Reports
</a>
{% endif %}
</div>
<h3>Packages</h3>

View File

@@ -18,16 +18,16 @@
{{ _("Package") }}
</div>
<div class="col-2 text-center">
Latest release / MB
Latest release (MB)
</div>
<div class="col-2 text-center">
Releases / MB
Releases (MB)
</div>
<div class="col-2 text-center">
Screenshots / MB
Screenshots (MB)
</div>
<div class="col-2 text-center">
Total / MB
Total (MB)
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
{%- endif %}
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=54">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=59">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
{% if noindex -%}
@@ -252,14 +252,14 @@
<footer class="my-5 pt-5">
<p class="pt-3 mb-1">
ContentDB &copy; 2018-23 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
ContentDB &copy; 2018-24 to <a href="{{ url_for('flatpage', path='about') }}">rubenwardy</a>
</p>
<ul class="list-inline my-1">
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='about') }}">{{ _("About") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/contact_us') }}">{{ _("Contact Us") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='rules') }}">{{ _("Rules") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='terms') }}">{{ _("Terms of Service") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}#contentdb">{{ _("Donate") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/api') }}">{{ _("API") }}</a></li>
@@ -274,7 +274,7 @@
<li class="list-inline-item"><a href="{{ url_for('collections.list_all') }}">{{ _("Collections") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}">{{ _("Support Creators") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('translate.translate') }}">{{ _("Translate Packages") }}</a></li>
<li class="list-inline-item"><a href="https://github.com/minetest/contentdb">{{ _("Source Code") }}</a></li>
<li class="list-inline-item"><a href="https://github.com/luanti-org/contentdb">{{ _("Source Code") }}</a></li>
</ul>
<form method="POST" action="{{ url_for('set_nonfree') }}" class="my-3">
@@ -285,9 +285,11 @@
<input type="submit" class="btn btn-sm btn-secondary" value="{{ _('Hide non-free packages') }}">
{% endif %}
</form>
<p class="text-warning">
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
</p>
{% if false %}
<p class="text-warning">
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
</p>
{% endif %}
{% if debug %}
<p style="color: red">

View File

@@ -5,7 +5,7 @@
{% endblock %}
{% block description %}
{{ _("Welcome to the best place to find Minetest mods, games, and texture packs") }}
{{ _("Welcome to the best place to find Luanti mods, games, and texture packs") }}
{% endblock %}
{% block scriptextra %}
@@ -13,10 +13,10 @@
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://content.minetest.net/",
"url": "https://content.luanti.org/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://content.minetest.net/packages?q={search_term_string}",
"target": "https://content.luanti.org/packages?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
@@ -39,24 +39,25 @@
</div>
<div class="carousel-inner">
{% for package in spotlight_pkgs %}
{% set meta = package.get_translated(load_desc=False) %}
{% set cover_image = package.get_cover_image_url() %}
{% set tags = package.tags | sort(attribute="views", reverse=True) %}
<div class="carousel-item {% if loop.index == 1 %}active{% endif %}">
<a href="{{ package.get_url('packages.view') }}">
<div class="ratio ratio-16x9">
<img src="{{ cover_image }}"
alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}">
alt="{{ _('%(title)s by %(author)s', title=meta.title, author=package.author.display_name) }}">
</div>
<div class="carousel-caption text-shadow">
<h3 class="mt-0 mb-3">
{% if package.author %}
{{ _('<strong>%(title)s</strong> by %(author)s', title=package.title, author=package.author.display_name) }}
{{ _('<strong>%(title)s</strong> by %(author)s', title=meta.title, author=package.author.display_name) }}
{% else %}
<strong>{{ package.title }}</strong>
<strong>{{ meta.title }}</strong>
{% endif %}
</h3>
<p>
{{ package.short_desc }}
{{ meta.short_desc }}
</p>
{% if package.author %}
<div class="d-none d-md-block">

View File

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

View File

@@ -176,11 +176,6 @@
<input class="btn btn-primary" name="btn_submit" type="submit" value="Comment" />
</form>
{% endif %}
{% if thread.private %}
<p class="text-muted card-body my-0 pt-0">
{{ _("You can add someone to a private thread by writing @username.") }}
</p>
{% endif %}
</div>
</div>
</div>

View File

@@ -14,7 +14,7 @@
[{{ topic.type.text }}]
</td>
<td>
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
<a href="https://forum.luanti.org/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
{% if topic.wip %}[{{ _("WIP") }}]{% endif %}
</td>
{% if show_author %}
@@ -42,7 +42,7 @@
{% macro render_topics(topics, current_user) -%}
<div class="list-group">
{% for topic in topics %}
<a class="list-group-item list-group-item-action" href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">
<a class="list-group-item list-group-item-action" href="https://forum.luanti.org/viewtopic.php?t={{ topic.topic_id}}">
<span class="float-end text-muted">
{{ topic.created_at | date }}
</span>

View File

@@ -23,7 +23,7 @@
{% for t in similar_topics %}
<li>
[{{ t.type.text }}]
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
<a href="https://forum.luanti.org/viewtopic.php?t={{ t.topic_id }}">
{{ _("%(title)s by %(display_name)s", title=t.title, display_name=t.author.display_name) }}
</a>
{% if t.wip %}[{{ _("WIP") }}]{% endif %}

View File

@@ -64,8 +64,8 @@
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ render_field(form.title) }}
{{ render_field(form.description, hint=_("Shown to users when you request access to their account")) }}
{{ render_field(form.title, hint=_("Titles must be globally unique. For example, what's the name of your application?")) }}
{{ render_field(form.description, hint=_("Shown to users when you request access to their account. For example, what does your application do?")) }}
{{ render_field(form.redirect_url) }}
{{ render_field(form.app_type, hint=_("Where will you store your client_secret?")) }}

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