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 ## 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. This is usually the latest `master` commit.
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -6,6 +6,8 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install docker-compose
run: sudo apt-get install -y docker-compose
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Copy config - name: Copy config
run: cp utils/ci/* . 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 && \ RUN addgroup --gid 5123 cdb && \
useradd -r -u 5123 -g cdb cdb adduser --uid 5123 -S cdb -G cdb
WORKDIR /home/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 mkdir /var/cdb
RUN chown -R cdb:cdb /var/cdb RUN chown -R cdb:cdb /var/cdb
COPY requirements.lock.txt requirements.lock.txt COPY requirements.lock.txt requirements.lock.txt
RUN pip install -r requirements.lock.txt RUN pip install -r requirements.lock.txt && \
RUN pip install gunicorn pip install gunicorn
COPY utils utils COPY utils utils
COPY config.cfg config.cfg COPY config.cfg config.cfg

View File

@@ -1,7 +1,7 @@
# ContentDB # 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+. Developed by rubenwardy, license AGPLv3.0+.
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment. 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" --> "*" Dependency
Package "1" --> "*" Tag Package "1" --> "*" Tag
Package "1" --> "*" MetaPackage : provides Package "1" --> "*" MetaPackage : provides
Release --> MinetestVersion Release --> LuantiVersion
Package --> License Package --> License
Dependency --> Package Dependency --> Package
Dependency --> MetaPackage 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 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_babel import Babel, gettext
from flask_flatpages import FlatPages from flask_flatpages import FlatPages
from flask_flatpages.utils import pygmented_markdown
from flask_github import GitHub from flask_github import GitHub
from flask_login import logout_user, current_user, LoginManager from flask_login import logout_user, current_user, LoginManager
from flask_mail import Mail from flask_mail import Mail
from flask_wtf.csrf import CSRFProtect 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 import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration from sentry_sdk.integrations.flask import FlaskIntegration
@@ -67,19 +66,18 @@ app = Flask(__name__, static_folder="public/static")
def my_flatpage_renderer(text): def my_flatpage_renderer(text):
# Render with jinja first # Render with jinja first
prerendered_body = render_template_string(text) 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_ROOT"] = "flatpages"
app.config["FLATPAGES_EXTENSION"] = ".md" 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["FLATPAGES_HTML_RENDERER"] = my_flatpage_renderer
app.config["WTF_CSRF_TIME_LIMIT"] = None app.config["WTF_CSRF_TIME_LIMIT"] = None
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "../translations" app.config["BABEL_TRANSLATION_DIRECTORIES"] = "../translations"
app.config["LANGUAGES"] = { app.config["LANGUAGES"] = {
"en": "English", "en": "English",
"cs": "čeština",
"de": "Deutsch", "de": "Deutsch",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@@ -90,6 +88,7 @@ app.config["LANGUAGES"] = {
"ru": "русский язык", "ru": "русский язык",
"sk": "Slovenčina", "sk": "Slovenčina",
"sv": "Svenska", "sv": "Svenska",
"ta": "தமிழ்",
"tr": "Türkçe", "tr": "Türkçe",
"uk": "Українська", "uk": "Українська",
"vi": "tiếng Việt", "vi": "tiếng Việt",

View File

@@ -1,248 +1,252 @@
# THIS FILE IS AUTOGENERATED: utils/extract_translations.py # 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 # NOTE: tags: title for 128px
pgettext("tags", "128px+") pgettext("tags", "128px+")
# NOTE: tags: description for 128px # NOTE: tags: description for 128px
pgettext("tags", "For 128px or higher texture packs") 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 # NOTE: tags: title for jam_game_2022
pgettext("tags", " Jam / Game 2022") pgettext("tags", " Jam / Game 2022")
# NOTE: tags: description for jam_game_2022 # NOTE: tags: description for jam_game_2022
pgettext("tags", "Entries to the 2022 Minetest Game Jam ") pgettext("tags", "Entries to the 2022 Minetest Game Jam ")
# NOTE: content_warnings: title for gore # NOTE: tags: title for jam_game_2023
pgettext("content_warnings", "Gore") pgettext("tags", "Jam / Game 2023")
# NOTE: content_warnings: description for gore # NOTE: tags: description for jam_game_2023
pgettext("content_warnings", "Blood, etc") pgettext("tags", "Entries to the 2023 Minetest Game Jam ")
# NOTE: content_warnings: title for gambling # NOTE: tags: title for jam_game_2024
pgettext("content_warnings", "Gambling") pgettext("tags", "Jam / Game 2024")
# NOTE: content_warnings: description for gambling # NOTE: tags: description for jam_game_2024
pgettext("content_warnings", "Games of chance, gambling games, etc") pgettext("tags", "Entries to the 2024 Luanti Game Jam")
# NOTE: content_warnings: title for violence # NOTE: tags: title for jam_weekly_2021
pgettext("content_warnings", "Violence") pgettext("tags", "Jam / Weekly Challenges 2021")
# NOTE: content_warnings: description for violence # NOTE: tags: description for jam_weekly_2021
pgettext("content_warnings", "Non-cartoon violence. May be towards fantasy or human-like characters") pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding event in 2021")
# NOTE: content_warnings: title for horror # NOTE: tags: title for less_than_px
pgettext("content_warnings", "Fear / Horror") pgettext("tags", "<16px")
# NOTE: content_warnings: description for horror # NOTE: tags: description for less_than_px
pgettext("content_warnings", "Shocking and scary content. May scare young children") pgettext("tags", "For less than 16px texture packs ")
# NOTE: content_warnings: title for bad_language # NOTE: tags: title for library
pgettext("content_warnings", "Bad Language") pgettext("tags", "API / Library")
# NOTE: content_warnings: description for bad_language # NOTE: tags: description for library
pgettext("content_warnings", "Contains swearing") 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 # NOTE: content_warnings: title for alcohol_tobacco
pgettext("content_warnings", "Alcohol / Tobacco") pgettext("content_warnings", "Alcohol / Tobacco")
# NOTE: content_warnings: description for alcohol_tobacco # NOTE: content_warnings: description for alcohol_tobacco
pgettext("content_warnings", "Contains alcohol and/or 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 # NOTE: content_warnings: title for drugs
pgettext("content_warnings", "Drugs") pgettext("content_warnings", "Drugs")
# NOTE: content_warnings: description for drugs # NOTE: content_warnings: description for drugs
pgettext("content_warnings", "Contains recreational drugs other than alcohol or tobacco") 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 import requests
from celery import group, uuid from celery import group, uuid
from flask import redirect, url_for, flash, current_app 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, \ 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.emails import send_pending_digests
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts 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, \ from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
import_languages, check_all_zip_files import_languages, check_all_zip_files
from app.tasks.usertasks import import_github_user_ids 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 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 from app.utils import add_notification, get_system_user
actions = {} actions = {}
@@ -68,9 +68,10 @@ def clean_uploads():
release_urls = get_filenames_from_column(PackageRelease.url) release_urls = get_filenames_from_column(PackageRelease.url)
screenshot_urls = get_filenames_from_column(PackageScreenshot.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) 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) unreachable = existing_uploads.difference(db_urls)
import sys 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"))) 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") @action("DANGER: Delete less popular removed packages")
def del_less_popular_removed_packages(): def del_less_popular_removed_packages():
task_id = uuid() task_id = uuid()
@@ -417,3 +425,10 @@ def delete_empty_threads():
def check_for_broken_links(): def check_for_broken_links():
for package in Package.query.filter_by(state=PackageState.APPROVED).all(): for package in Package.query.filter_by(state=PackageState.APPROVED).all():
check_package_for_broken_links.delay(package.id) 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 wtforms.validators import InputRequired, Length, Optional
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \ from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
get_int_or_abort get_int_or_abort
from sqlalchemy import func
from . import bp from . import bp
from .actions import actions 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 from ...querybuilder import QueryBuilder
@@ -182,6 +183,17 @@ def transfer():
return render_template("admin/transfer.html", form=form) 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/") @bp.route("/admin/storage/")
@rank_required(UserRank.EDITOR) @rank_required(UserRank.EDITOR)
def storage(): def storage():
@@ -192,15 +204,20 @@ def storage():
show_all = len(packages) < 100 show_all = len(packages) < 100
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50) 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 = [] data = []
for package in packages: for package in packages:
size_releases = sum([x.file_size_bytes for x in package.releases]) size_releases = package_size_releases.get(package.id, 0)
size_screenshots = sum([x.file_size_bytes for x in package.screenshots]) 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() latest_release = package.releases.first()
size_latest = latest_release.file_size_bytes if latest_release else 0 size_latest = latest_release.file_size_bytes if latest_release else 0
size_total = size_releases + size_screenshots data.append([package, size_total, size_releases, size_screenshots, size_latest])
if size_total > min_size*1024*1024:
data.append([package, size_total, size_releases, size_screenshots, size_latest])
data.sort(key=lambda x: x[1], reverse=True) data.sort(key=lambda x: x[1], reverse=True)
return render_template("admin/storage.html", data=data) 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template, request, abort from flask import render_template, request, abort
from flask_babel import lazy_gettext
from flask_login import current_user, login_required 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.models import db, AuditLogEntry, UserRank, User, Permission
from app.utils import rank_required, get_int_or_abort 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 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/") @bp.route("/admin/audit/")
@rank_required(UserRank.MODERATOR) @rank_required(UserRank.APPROVER)
def audit(): def audit():
page = get_int_or_abort(request.args.get("page"), 1) page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 100)) num = min(40, get_int_or_abort(request.args.get("n"), 100))
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at)) query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
if "username" in request.args: form = AuditForm(request.args)
user = User.query.filter_by(username=request.args.get("username")).first() username = form.username.data
if not user: q = form.q.data
abort(404) url = form.url.data
if username:
user = User.query.filter_by(username=username).first_or_404()
query = query.filter_by(causer=user) query = query.filter_by(causer=user)
if "q" in request.args: if q:
q = request.args["q"]
query = query.filter(AuditLogEntry.title.ilike(f"%{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) 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_>/") @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 app.utils import rank_required, add_audit_log
from . import bp from . import bp
from app.models import UserRank, MinetestRelease, db, AuditSeverity from app.models import UserRank, LuantiRelease, db, AuditSeverity
@bp.route("/versions/") @bp.route("/versions/")
@rank_required(UserRank.MODERATOR) @rank_required(UserRank.MODERATOR)
def version_list(): def version_list():
return render_template("admin/versions/list.html", 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): class VersionForm(FlaskForm):
@@ -45,14 +45,14 @@ class VersionForm(FlaskForm):
def create_edit_version(name=None): def create_edit_version(name=None):
version = None version = None
if name is not 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: if version is None:
abort(404) abort(404)
form = VersionForm(formdata=request.form, obj=version) form = VersionForm(formdata=request.form, obj=version)
if form.validate_on_submit(): if form.validate_on_submit():
if version is None: if version is None:
version = MinetestRelease(form.name.data) version = LuantiRelease(form.name.data)
db.session.add(version) db.session.add(version)
flash("Created version " + form.name.data, "success") 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.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats
from app.markdown import render_markdown from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \ 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 PackageAlias, Language
from app.querybuilder import QueryBuilder 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, \ from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date, cached, \
cors_allowed 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 . import bp
from .auth import is_api_authd from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \ from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
@@ -102,7 +102,7 @@ def package_view_client(package: Package):
protocol_version = request.args.get("protocol_version") protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version") engine_version = request.args.get("engine_version")
if protocol_version or 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: else:
version = None version = None
@@ -113,9 +113,10 @@ def package_view_client(package: Package):
formspec_version = get_int_or_abort(request.args["formspec_version"]) formspec_version = get_int_or_abort(request.args["formspec_version"])
include_images = is_yes(request.args.get("include_images", "true")) 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) 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) 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): def package_hypertext(package):
formspec_version = get_int_or_abort(request.args["formspec_version"]) formspec_version = get_int_or_abort(request.args["formspec_version"])
include_images = is_yes(request.args.get("include_images", "true")) 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) 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"]) @bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
@@ -569,14 +570,14 @@ def package_scores():
@cors_allowed @cors_allowed
@cached(60*60) @cached(60*60)
def tags(): 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/") @bp.route("/api/content_warnings/")
@cors_allowed @cors_allowed
@cached(60*60) @cached(60*60)
def content_warnings(): 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/") @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/") @bp.route("/api/minetest_versions/")
@cors_allowed @cors_allowed
def versions(): def versions():
protocol_version = request.args.get("protocol_version") protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version") engine_version = request.args.get("engine_version")
if protocol_version or 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: if rel is None:
error(404, "No releases found") error(404, "No releases found")
return jsonify(rel.as_dict()) return jsonify(rel.as_dict())
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/") @bp.route("/api/languages/")
@@ -852,7 +835,7 @@ def hypertext():
if request.content_type == "text/markdown": if request.content_type == "text/markdown":
html = render_markdown(html) 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/") @bp.route("/api/collections/")
@@ -903,9 +886,9 @@ def collection_view(token, author, name):
@cached(300) @cached(300)
def updates(): def updates():
protocol_version = get_int_or_abort(request.args.get("protocol_version")) protocol_version = get_int_or_abort(request.args.get("protocol_version"))
minetest_version = request.args.get("engine_version") engine_version = request.args.get("engine_version")
if protocol_version or minetest_version: if protocol_version or engine_version:
version = MinetestRelease.get(minetest_version, protocol_version) version = LuantiRelease.get(engine_version, protocol_version)
else: else:
version = None 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.packages import do_edit_package
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image 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): 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, 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): if not token.can_operate_on_package(package):
error(403, "API token does not have access to the 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, 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): if not token.can_operate_on_package(package):
error(403, "API token does not have access to the 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 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({ return jsonify({
"success": True, "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 re
import typing 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_babel import lazy_gettext, gettext
from flask_login import current_user, login_required from flask_login import current_user, login_required
from flask_wtf import FlaskForm 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 wtforms.validators import InputRequired, Length, Optional, Regexp
from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity 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 from app.utils.models import is_package_page, add_audit_log, create_session
bp = Blueprint("collections", __name__) bp = Blueprint("collections", __name__)
@@ -70,7 +70,10 @@ def view(author, name):
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION): 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)] 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): class CollectionForm(FlaskForm):

View File

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

View File

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

View File

@@ -194,6 +194,10 @@ def create_edit_client(username, id_=None):
if form.validate_on_submit(): if form.validate_on_submit():
if is_new: 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() client = OAuthClient()
db.session.add(client) db.session.add(client)
client.owner = user client.owner = user
@@ -201,6 +205,7 @@ def create_edit_client(username, id_=None):
client.secret = random_string(32) client.secret = random_string(32)
client.approved = current_user.rank.at_least(UserRank.EDITOR) client.approved = current_user.rank.at_least(UserRank.EDITOR)
form.populate_obj(client) form.populate_obj(client)
verb = "Created" if is_new else "Edited" 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 wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
from . import bp 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): def make_label(obj: Tag | ContentWarning):
@@ -74,8 +74,8 @@ class AdvancedSearchForm(FlaskForm):
allow_blank=True, blank_value="", allow_blank=True, blank_value="",
get_pk=lambda a: a.id, get_label=lambda a: a.title) get_pk=lambda a: a.id, get_label=lambda a: a.title)
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()]) hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
engine_version = QuerySelectField(lazy_gettext("Minetest Version"), engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)), query_factory=lambda: LuantiRelease.query.order_by(db.asc(LuantiRelease.id)),
allow_blank=True, blank_value="", allow_blank=True, blank_value="",
get_pk=lambda a: a.value, get_label=lambda a: a.name) get_pk=lambda a: a.value, get_label=lambda a: a.name)
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[ 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"))]) 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)]) 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) 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) 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( flash(
gettext("Package already exists, but is removed. Please contact ContentDB staff to restore the package"), gettext("Package already exists, but is removed. Please contact ContentDB staff to restore the package"),
"danger") "danger")
return redirect(url_for("report.report", url=package.get_url("packages.view")))
else: else:
flash(markupsafe.Markup( flash(markupsafe.Markup(
f"<a class='btn btn-sm btn-danger float-end' href='{package.get_url('packages.view')}'>View</a>" + 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, "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: if wasNew and package.repo is not None:
import_repo_screenshot.delay(package.id) 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) return redirect(next_url)
except LogicError as e: except LogicError as e:
flash(e.message, "danger") flash(e.message, "danger")
db.session.rollback()
@bp.route("/packages/new/", methods=["GET", "POST"]) @bp.route("/packages/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
@login_required @login_required
def create_edit(author=None, name=None): 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") 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")) return redirect(url_for("users.email_notifications"))
@@ -458,6 +456,7 @@ def move_to_state(package):
@is_package_page @is_package_page
def translation(package): def translation(package):
return render_template("packages/translation.html", package=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") tabs=get_package_tabs(current_user, package), current_tab="translation")
@@ -570,7 +569,7 @@ def edit_maintainers(package):
for user in users: for user in users:
if not user in package.maintainers: if not user in package.maintainers:
if thread: if thread and user not in thread.watchers:
thread.watchers.append(user) thread.watchers.append(user)
add_notification(user, current_user, NotificationType.MAINTAINER, add_notification(user, current_user, NotificationType.MAINTAINER,
"Added you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package) "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 wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release 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 PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
from app.rediscache import has_key, set_temp_key, make_download_key from app.rediscache import has_key, set_temp_key, make_download_key
from app.tasks.importtasks import check_update_config from app.tasks.importtasks import check_update_config
@@ -42,11 +42,11 @@ def list_releases(package):
def get_mt_releases(is_max): 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: if is_max:
query = query.limit(query.count() - 1) query = query.limit(query.count() - 1)
else: else:
query = query.filter(MinetestRelease.name != "0.4.17") query = query.filter(LuantiRelease.name != "0.4.17")
return query return query
@@ -59,9 +59,9 @@ class CreatePackageReleaseForm(FlaskForm):
upload_mode = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload") 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) vcs_label = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None)
file_upload = FileField(lazy_gettext("File Upload")) 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) 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) query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@@ -74,9 +74,9 @@ class EditPackageReleaseForm(FlaskForm):
url = StringField(lazy_gettext("URL"), [Optional()]) url = StringField(lazy_gettext("URL"), [Optional()])
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None]) task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
approved = BooleanField(lazy_gettext("Is Approved")) 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) 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) query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@@ -85,7 +85,7 @@ class EditPackageReleaseForm(FlaskForm):
@login_required @login_required
@is_package_page @is_package_page
def create_release(package): 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") 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")) 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 ip = request.headers.get("X-Forwarded-For") or request.remote_addr
if ip is not None and not is_user_bot(): 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") reason = request.args.get("reason")
PackageDailyStats.update(package, is_minetest, reason) PackageDailyStats.update(package, is_luanti, reason)
key = make_download_key(ip, release.package) key = make_download_key(ip, release.package)
if not has_key(key): if not has_key(key):
@@ -214,10 +215,10 @@ def edit_release(package, id):
class BulkReleaseForm(FlaskForm): class BulkReleaseForm(FlaskForm):
set_min = BooleanField(lazy_gettext("Set Min")) 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) 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")) 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) 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")) only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
submit = SubmitField(lazy_gettext("Update")) 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_babel import lazy_gettext, gettext
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileRequired
from wtforms import StringField, SubmitField, BooleanField, FileField 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 wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
@@ -32,7 +33,7 @@ from app.utils import is_package_page
class CreateScreenshotForm(FlaskForm): class CreateScreenshotForm(FlaskForm):
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)]) 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")) submit = SubmitField(lazy_gettext("Save"))

View File

@@ -14,24 +14,30 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # 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_babel import lazy_gettext
from flask_login import current_user from flask_login import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from werkzeug.utils import redirect from werkzeug.utils import redirect
from wtforms import TextAreaField, SubmitField from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length, Optional, DataRequired
from app.models import User, UserRank from app.logic.uploads import upload_file
from app.tasks.emails import send_user_email from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
from app.tasks.webhooktasks import post_discord_webhook 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__) bp = Blueprint("report", __name__)
class ReportForm(FlaskForm): 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")) submit = SubmitField(lazy_gettext("Report"))
@@ -46,22 +52,131 @@ def report():
url = abs_url_samesite(url) 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(): 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: 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: 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 if form.file_upload.data:
for admin in User.query.filter_by(rank=UserRank.ADMIN).all(): atmt = ReportAttachment()
task = send_user_email.delay(admin.email, admin.locale or "en", report.attachments.add(atmt)
f"User report from {user_info}", text) 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, \ from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains, \
normalize_line_endings normalize_line_endings
from flask_wtf import FlaskForm 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 wtforms.validators import InputRequired, Length
from app.utils import get_int_or_abort from app.utils import get_int_or_abort
@@ -254,6 +254,9 @@ def view(id):
if mentioned is None: if mentioned is None:
continue continue
if not thread.check_perm(mentioned, Permission.SEE_THREAD):
continue
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title) msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY, add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
msg, thread.get_view_url(), thread.package) msg, thread.get_view_url(), thread.package)
@@ -281,7 +284,6 @@ def view(id):
class ThreadForm(FlaskForm): class ThreadForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)]) title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)])
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings]) 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")) btn_submit = SubmitField(lazy_gettext("Open Thread"))
@@ -296,14 +298,11 @@ def new():
if package is None: if package is None:
abort(404) abort(404)
def_is_private = request.args.get("private") or False
if package is None and not current_user.rank.at_least(UserRank.APPROVER): if package is None and not current_user.rank.at_least(UserRank.APPROVER):
abort(404) abort(404)
is_review_thread = package and not package.approved is_review_thread = package and not package.approved
allow_private_change = not is_review_thread is_private_thread = is_review_thread
if is_review_thread:
def_is_private = True
# Check that user can make the thread # Check that user can make the thread
if package and not package.check_perm(current_user, Permission.CREATE_THREAD): if package and not package.check_perm(current_user, Permission.CREATE_THREAD):
@@ -326,7 +325,6 @@ def new():
# Set default values # Set default values
elif request.method == "GET": elif request.method == "GET":
form.private.data = def_is_private
form.title.data = request.args.get("title") or "" form.title.data = request.args.get("title") or ""
# Validate and submit # Validate and submit
@@ -337,7 +335,7 @@ def new():
thread = Thread() thread = Thread()
thread.author = current_user thread.author = current_user
thread.title = form.title.data 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 thread.package = package
db.session.add(thread) db.session.add(thread)
@@ -367,7 +365,8 @@ def new():
add_notification(mentioned, current_user, NotificationType.NEW_THREAD, add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.get_view_url(), thread.package) 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) notif_msg = "New thread '{}'".format(thread.title)
if package is not None: if package is not None:
@@ -384,7 +383,7 @@ def new():
return redirect(thread.get_view_url()) 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/") @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_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): def mkdir(path):
@@ -76,10 +80,10 @@ def find_source_file(img):
period = source_filepath.rfind(".") period = source_filepath.rfind(".")
start = source_filepath[:period] start = source_filepath[:period]
ext = source_filepath[period + 1:] ext = source_filepath[period + 1:]
if ext not in ALLOWED_EXTENSIONS: if ext not in ALLOWED_MIMETYPES:
abort(404) abort(404)
for other_ext in ALLOWED_EXTENSIONS: for other_ext in ALLOWED_MIMETYPES.keys():
other_path = f"{start}.{other_ext}" other_path = f"{start}.{other_ext}"
if ext != other_ext and os.path.isfile(other_path): if ext != other_ext and os.path.isfile(other_path):
return other_path return other_path
@@ -87,6 +91,15 @@ def find_source_file(img):
abort(404) 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>") @bp.route("/thumbnails/<int:level>/<img>")
def make_thumbnail(img, level): def make_thumbnail(img, level):
if level > len(ALLOWED_RESOLUTIONS) or level <= 0: if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
@@ -104,7 +117,7 @@ def make_thumbnail(img, level):
source_filepath = find_source_file(img) source_filepath = find_source_file(img)
resize_and_crop(source_filepath, cache_filepath, (w, h)) 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 res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res return res

View File

@@ -20,7 +20,7 @@ from flask_login import current_user, login_required
from sqlalchemy import or_, and_ from sqlalchemy import or_, and_
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \ 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.querybuilder import QueryBuilder
from app.utils import get_int_or_abort, is_yes, rank_required from app.utils import get_int_or_abort, is_yes, rank_required
from . import bp from . import bp
@@ -83,11 +83,13 @@ def view_editor():
.order_by(db.desc(AuditLogEntry.created_at)) \ .order_by(db.desc(AuditLogEntry.created_at)) \
.limit(20).all() .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", return render_template("todo/editor.html", current_tab="editor",
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots, 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, 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, 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/") @bp.route("/todo/tags/")
@@ -170,7 +172,7 @@ def screenshots():
def mtver_support(): def mtver_support():
is_mtm_only = is_yes(request.args.get("mtm")) 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) \ query = db.session.query(Package) \
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \ .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()]) email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)]) password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()]) 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")) submit = SubmitField(lazy_gettext("Register"))
@@ -118,6 +118,8 @@ def handle_register(form):
return user return user
elif user is None: elif user is None:
return return
elif form.first_name.data != "":
abort(500)
user.password = make_flask_login_password(form.password.data) user.password = make_flask_login_password(form.password.data)

View File

@@ -77,7 +77,7 @@ def claim_forums():
# Get signature # Get signature
try: try:
profile = get_profile("https://forum.minetest.net", username) profile = get_profile("https://forum.luanti.org", username)
sig = profile.signature if profile else None sig = profile.signature if profile else None
except IOError as e: except IOError as e:
if hasattr(e, 'message'): 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]: 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: if token.package:
packages = [token.package] packages = [token.package]
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE): 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.logic.users import create_user
from app.models import db, User, APIToken, AuditSeverity 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 app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
from . import bp from . import bp
from .common import get_packages_for_vcs_and_token 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") flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
return redirect(url_for("users.login")) 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", add_audit_log(AuditSeverity.USER, user_by_github, "Logged in using GitHub OAuth",
url_for("users.profile", username=user_by_github.username)) url_for("users.profile", username=user_by_github.username))
db.session.commit() db.session.commit()

View File

@@ -38,7 +38,7 @@ def webhook_impl():
if token is None: if token is None:
return error(403, "Invalid authentication") 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: for package in packages:
# #
# Check event # Check event

View File

@@ -14,27 +14,27 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from celery import uuid from celery import uuid
from flask import Blueprint, render_template, redirect, request, abort, url_for from flask import Blueprint, render_template, redirect, request, abort, url_for
from flask_babel import lazy_gettext from flask_babel import lazy_gettext
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField from wtforms import StringField, BooleanField, SubmitField, SelectMultipleField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length, Optional
from app.tasks import celery from app.tasks import celery
from app.utils import rank_required from app.utils import rank_required
bp = Blueprint("zipgrep", __name__) 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 from app.tasks.zipgrep import search_in_releases
class SearchForm(FlaskForm): class SearchForm(FlaskForm):
query = StringField(lazy_gettext("Text to find (regex)"), [InputRequired(), Length(1, 100)]) 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") 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")) submit = SubmitField(lazy_gettext("Search"))
@@ -44,7 +44,7 @@ def zipgrep_search():
form = SearchForm(request.form) form = SearchForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
task_id = uuid() 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) result_url = url_for("zipgrep.view_results", id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=result_url)) return redirect(url_for("tasks.check", id=task_id, r=result_url))

View File

@@ -16,7 +16,7 @@
import datetime 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 PackageState, PackageType, PackageRelease, MetaPackage, Dependency
from .utils import make_flask_login_password from .utils import make_flask_login_password
@@ -35,12 +35,12 @@ def populate(session):
system_user.rank = UserRank.BOT system_user.rank = UserRank.BOT
session.add(system_user) session.add(system_user)
session.add(MinetestRelease("None", 0)) session.add(LuantiRelease("None", 0))
session.add(MinetestRelease("0.4.16/17", 32)) session.add(LuantiRelease("0.4.16/17", 32))
session.add(MinetestRelease("5.0", 37)) session.add(LuantiRelease("5.0", 37))
session.add(MinetestRelease("5.1", 38)) session.add(LuantiRelease("5.1", 38))
session.add(MinetestRelease("5.2", 39)) session.add(LuantiRelease("5.2", 39))
session.add(MinetestRelease("5.3", 39)) session.add(LuantiRelease("5.3", 39))
tags = {} tags = {}
for tag in ["Inventory", "Mapgen", "Building", 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() } licenses = { x.name : x for x in License.query.all() }
tags = { x.name : x for x in Tag.query.all() } tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first() admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
v4 = MinetestRelease.query.filter_by(protocol=32).first() v4 = LuantiRelease.query.filter_by(protocol=32).first()
v51 = MinetestRelease.query.filter_by(protocol=38).first() v51 = LuantiRelease.query.filter_by(protocol=38).first()
ez = User("Shara") ez = User("Shara")
ez.github_username = "Ezhh" 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. 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/luanti-org/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/issues/" class="btn btn-secondary me-1">Issue tracker</a>
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a> <a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
{% if monitoring_url -%} {% if monitoring_url -%}
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a> <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 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. 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, **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 You should read
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/) [the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest. for a guide to making mods and games using Luanti.
<h2 id="donate">How can I support / donate to ContentDB?</h2> <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 ## 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. This provides us with improved error logging and performance insights.

View File

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

View File

@@ -3,7 +3,7 @@ title: API
## Resources ## 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 ## 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: Not all endpoints require authentication, but it is done using Bearer tokens:
```bash ```bash
curl https://content.minetest.net/api/whoami/ \ curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN" -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. * DELETE `/api/delete-token/`: Deletes the currently used token.
```bash ```bash
# Logout # Logout
curl -X DELETE https://content.minetest.net/api/delete-token/ \ curl -X DELETE https://content.luanti.org/api/delete-token/ \
-H "Authorization: Bearer YOURTOKEN" -H "Authorization: Bearer YOURTOKEN"
``` ```
@@ -78,9 +78,12 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* GET `/api/packages/` (List) * GET `/api/packages/` (List)
* See [Package Queries](#package-queries) * See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/` (Read) * 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) * PUT `/api/packages/<author>/<name>/` (Update)
* Requires authentication. * 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`. * `type`: One of `GAME`, `MOD`, `TXP`.
* `title`: Human-readable title. * `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved). * `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. * `video_url`: URL to a video.
* `donate_url`: URL to a donation page. * `donate_url`: URL to a donation page.
* `translation_url`: URL to send users interested in translating your package. * `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/` * 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. * `long_description` is given as a hypertext object, see `/hypertext/` below.
* `info_hypertext` is the info sidebar as a hypertext object. * `info_hypertext` is the info sidebar as a hypertext object.
* Query arguments * 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. * `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. * `protocol_version`: Optional, used to get the correct release.
* `engine_version`: Optional, used to get the correct release. Ex: `5.3.0`. * `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/` * 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. to be used in a `hypertext` formspec element.
* Query arguments: * Query arguments:
* `formspec_version`: Required, maximum supported formspec version. * `formspec_version`: Required, maximum supported formspec version.
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked. * `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. * `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
* `body`: markup for long description. * `body`: markup for long description.
* `links`: dictionary of anchor name to link URL. * `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. * `image_tooltips`: dictionary of img name to tooltip text.
* GET `/api/packages/<username>/<name>/dependencies/` * GET `/api/packages/<username>/<name>/dependencies/`
* Returns dependencies, with suggested candidates * 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: You can download a package by building one of the two URLs:
``` ```
https://content.minetest.net/packages/${author}/${name}/download/` https://content.luanti.org/packages/${author}/${name}/download/`
https://content.minetest.net/packages/${author}/${name}/releases/${release}/download/` https://content.luanti.org/packages/${author}/${name}/releases/${release}/download/`
``` ```
Examples: Examples:
```bash ```bash
# Edit package # 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" \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }' -d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }'
# Remove website URL # 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" \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "website": null }' -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` * `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). * `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`. * `lang`: Filter by translation support, eg: `en`/`de`/`ja`/`zh_TW`.
* `protocol_version`: Only show packages supported by this Minetest protocol version. * `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Minetest engine version, eg: `5.3.0`. * `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
Sorting query parameters: Sorting query parameters:
@@ -212,7 +233,7 @@ Format query parameters:
* `limit`: Return at most `limit` packages. * `limit`: Return at most `limit` packages.
* `fmt`: How the response is formatted. * `fmt`: How the response is formatted.
* `keys`: author/name only. * `keys`: author/name only.
* `short`: stuff needed for the Minetest client. * `short`: stuff needed for the Luanti client.
* `vcs`: `short` but with `repo`. * `vcs`: `short` but with `repo`.
@@ -232,8 +253,8 @@ Format query parameters:
* `url`: download URL * `url`: download URL
* `commit`: commit hash or null * `commit`: commit hash or null
* `downloads`: number of downloads * `downloads`: number of downloads
* `min_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 minetest version (inclusive). * `max_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `size`: size of zip file, in bytes. * `size`: size of zip file, in bytes.
* `package` * `package`
* `author`: author username * `author`: author username
@@ -242,8 +263,8 @@ Format query parameters:
* GET `/api/updates/` (Look-up table) * GET `/api/updates/` (Look-up table)
* Returns a look-up table from package key (`author/name`) to latest release id * Returns a look-up table from package key (`author/name`) to latest release id
* Query arguments * Query arguments
* `protocol_version`: Only show packages supported by this Minetest protocol version. * `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Minetest engine version, eg: `5.3.0`. * `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
* GET `/api/packages/<username>/<name>/releases/` (List) * GET `/api/packages/<username>/<name>/releases/` (List)
* Returns array of release dictionaries, see above, but without package info. * Returns array of release dictionaries, see above, but without package info.
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read) * GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
@@ -258,7 +279,7 @@ Format query parameters:
* For zip upload release creation: * For zip upload release creation:
* `file`: multipart file to upload, like `<input type="file" name="file">`. * `file`: multipart file to upload, like `<input type="file" name="file">`.
* `commit`: (Optional) Source Git commit hash, for informational purposes. * `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) * DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
* Requires authentication. * Requires authentication.
* Deletes release. * Deletes release.
@@ -267,7 +288,7 @@ Examples:
```bash ```bash
# Create release from Git # Create release from Git
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \ curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ -d '{
"method": "git", "method": "git",
@@ -278,17 +299,17 @@ curl -X POST https://content.minetest.net/api/packages/username/name/releases/ne
}' }'
# Create release from zip upload # 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" \ -H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip -F title="My Release" -F file=@path/to/file.zip
# Create release from zip upload with commit hash # 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" \ -H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F commit="8ef74deec170a8ce789f6055a59d43876d16a7ea" -F file=@path/to/file.zip -F title="My Release" -F commit="8ef74deec170a8ce789f6055a59d43876d16a7ea" -F file=@path/to/file.zip
# Delete release # 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" -H "Authorization: Bearer YOURTOKEN"
``` ```
@@ -329,26 +350,26 @@ Examples:
```bash ```bash
# Create screenshot # 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" \ -H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png -F title="My Release" -F file=@path/to/screnshot.png
# Create screenshot and set it as the cover image # 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" \ -H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true" -F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
# Delete screenshot # 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" -H "Authorization: Bearer YOURTOKEN"
# Reorder screenshots # 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" \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]" -d "[13, 2, 5, 7]"
# Set cover image # 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" \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{ 'cover_image': 123 }" -d "{ 'cover_image': 123 }"
``` ```
@@ -458,7 +479,7 @@ Supported query parameters:
## Collections ## Collections
* GET `/api/collections/` * GET `/api/collections/`
* Query args: * Query args:
* `author`: collection author username. * `author`: collection author username.
* `package`: collections that contain the package. * `package`: collections that contain the package.
* Returns JSON array of collection entries: * Returns JSON array of collection entries:
@@ -468,7 +489,7 @@ Supported query parameters:
* `short_description` * `short_description`
* `created_at`: creation time in iso format. * `created_at`: creation time in iso format.
* `private`: whether collection is private, boolean. * `private`: whether collection is private, boolean.
* `package_count`: number of packages, integer. * `package_count`: number of packages, integer.
* GET `/api/collections/<username>/<name>/` * GET `/api/collections/<username>/<name>/`
* Returns JSON object for collection: * Returns JSON object for collection:
* `author`: author username. * `author`: author username.
@@ -498,7 +519,7 @@ Supported query parameters:
### Content Warnings ### Content Warnings
* GET `/api/content_warnings/` ([View](/api/content_warnings/)) * GET `/api/content_warnings/` ([View](/api/content_warnings/))
* List of objects with * List of objects with
* `name`: technical name * `name`: technical name
* `title`: human-readable title * `title`: human-readable title
* `description`: tag description or null * `description`: tag description or null
@@ -506,14 +527,14 @@ Supported query parameters:
### Licenses ### Licenses
* GET `/api/licenses/` ([View](/api/licenses/)) * GET `/api/licenses/` ([View](/api/licenses/))
* List of objects with: * List of objects with:
* `name` * `name`
* `is_foss`: whether the license is foss * `is_foss`: whether the license is foss
### Minetest Versions ### Luanti Versions
* GET `/api/minetest_versions/` ([View](/api/minetest_versions/)) * GET `/api/minetest_versions/` ([View](/api/minetest_versions/))
* List of objects with: * List of objects with:
* `name`: Version name. * `name`: Version name.
* `is_dev`: boolean, is dev version. * `is_dev`: boolean, is dev version.
* `protocol_version`: protocol version number. * `protocol_version`: protocol version number.
@@ -521,7 +542,7 @@ Supported query parameters:
### Languages ### Languages
* GET `/api/languages/` ([View](/api/languages/)) * GET `/api/languages/` ([View](/api/languages/))
* List of objects with: * List of objects with:
* `id`: language code. * `id`: language code.
* `title`: native language name. * `title`: native language name.
* `has_contentdb_translation`: whether ContentDB has been translated into this language. * `has_contentdb_translation`: whether ContentDB has been translated into this language.
@@ -552,13 +573,11 @@ Supported query parameters:
* `pop_txp`: popular textures * `pop_txp`: popular textures
* `pop_game`: popular games * `pop_game`: popular games
* `high_reviewed`: highest reviewed * `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 `/api/cdb_schema/` ([View](/api/cdb_schema/))
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings. * Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
* See [JSON Schema Reference](https://json-schema.org/). * See [JSON Schema Reference](https://json-schema.org/).
* POST `/api/hypertext/` * 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. to be used in a `hypertext` formspec element.
* Post data: HTML or Markdown as plain text. * Post data: HTML or Markdown as plain text.
* Content-Type: `text/html` or `text/markdown`. * 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! of characters, use them wisely!
```ini ```ini
# Bad, we know this is a mod for Minetest. Doesn't give much information other than "food" # Bad, we know this is a mod for Luanti. Doesn't give much information other than "food"
description = The food mod for Minetest description = The food mod for Luanti
# Much better, says what is actually in this mod! # Much better, says what is actually in this mod!
description = Adds soup, cakes, bakes and juices 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 a user sees before clicking on your package. Make sure it's possible to tell what a
thumbnail is when it's small. 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. Edit Package > Screenshots.
## 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, 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. 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 example of what to do. For inspiration, you might want to look at how games on
Steam write their descriptions. 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) * API reference (unless your mod is a library only)
* Development instructions for your package (this should be in the repo's README) * 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) * 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. when support for showing the long description is added.
## Localize / Translate your package ## 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 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. can also translate your ContentDB page. See Edit Package > Translation for more information.
<p> <p>
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html"> <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>
<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") }} {{ _("Translating content meta - lua_api.md") }}
</a> </a>
</p> </p>

View File

@@ -6,7 +6,7 @@ your client to use new flags.
## 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: client:
``` ```
@@ -17,7 +17,7 @@ A flag can be:
* `nonfree`: can be used to hide packages which do not qualify as * `nonfree`: can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation. '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 * `deprecated`: packages marked as Deprecated
* A content warning, given below. * A content warning, given below.
* `*`: hides all content warnings. * `*`: hides all content warnings.
@@ -33,8 +33,8 @@ without making a release.
Packages with mature content will be tagged with a content warning based Packages with mature content will be tagged with a content warning based
on the content type. on the content type.
* `alcohol_tobacco`: alcohol or tobacco.
* `bad_language`: swearing. * `bad_language`: swearing.
* `drugs`: drugs or alcohol.
* `gambling` * `gambling`
* `gore`: blood, etc. * `gore`: blood, etc.
* `horror`: shocking and scary content. * `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_*.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.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. * 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: 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 * [Kenney game assets](https://www.kenney.nl/assets) - everything
* [Free Sound](https://freesound.org/) - sounds * [Free Sound](https://freesound.org/) - sounds
* [PolyHaven](https://polyhaven.com/) - 3d models and textures. * [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. 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). 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? ## 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. 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. 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. * [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. * 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 You should read
[the official Minetest Modding Book](https://rubenwardy.com/minetest_modding_book/) [the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Minetest. for a guide to making mods and games using Luanti.
### How do I install something from here? ### How do I install something from here?

View File

@@ -7,10 +7,10 @@ title: Featured Packages
## What are Featured Packages? ## What are Featured Packages?
Featured Packages are shown at the top of the ContentDB homepage. In the future, 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 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 The featured content should be content that we are comfortable recommending to
a first time player. 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: 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: 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: Use public source control (such as Git).
* SHOULD: Have at least 3 reviews, and be largely positive. * SHOULD: Have at least 3 reviews, and be largely positive.
@@ -94,7 +94,7 @@ is available.
### Usability ### Usability
* MUST: Unsupported mapgens are disabled in game.conf. * 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. stuck within the first 5 minutes of playing.
* SHOULD: Have good documentation. This may include one or more of: * SHOULD: Have good documentation. This may include one or more of:
* A craftguide, or other in-game learning system * 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: Follow new releases for a package:
``` ```
https://content.minetest.net/packages/AUTHOR/NAME/releases_feed.atom https://content.luanti.org/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.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. with, which is useful for overriding ContentDB's automatic detection.
Both of these are comma-separated lists of game technical ids. Any `_game` 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 supported_games = minetest_game, repixture
unsupported_games = lordofthetest, nodecore, whynot unsupported_games = lordofthetest, nodecore, whynot

View File

@@ -1,5 +1,5 @@
title: How to install mods, games, and texture packs 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) ## 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 1. Open the mainmenu
2. Go to the Content tab and click "Browse online content". 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. 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". 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. 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". 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"> <div class="col-md-6">
<figure> <figure>
<a href="/static/installing_content_tab.png"> <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> </a>
<figcaption class="text-muted ps-1"> <figcaption class="text-muted ps-1">
1. Click Browser Online Content in the content tab. 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"> <div class="col-md-6">
<figure> <figure>
<a href="/static/installing_cdb_dialog.png"> <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> </a>
<figcaption class="text-muted ps-1"> <figcaption class="text-muted ps-1">
2. Search for the package and click "Install". 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: Troubleshooting:
* I can't find it in the ContentDB dialog (Browse online content) * 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, * 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. 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, * 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. 1. Mods: Enable the content using "Select Mods" when selecting a world.
2. Games: choose a game when making 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="row mt-5">
<div class="col-md-6"> <div class="col-md-6">
<figure> <figure>
<a href="/static/installing_select_mods.png"> <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> </a>
<figcaption class="text-muted ps-1"> <figcaption class="text-muted ps-1">
Enable mods using the Select Mods dialog. Enable mods using the Select Mods dialog.
@@ -76,7 +76,7 @@ Troubleshooting:
3. Find the user data directory. 3. Find the user data directory.
In 5.4.0 and above, you can click "Open user data directory" in the Credits tab. In 5.4.0 and above, you can click "Open user data directory" in the Credits tab.
Otherwise: Otherwise:
* Windows: whereever you extracted or installed Minetest to. * Windows: wherever you extracted or installed Luanti to.
* Linux: usually `~/.minetest/` * Linux: usually `~/.minetest/`
4. Open or create the folder for the type of content (`mods`, `games`, or `textures`) 4. Open or create the folder for the type of content (`mods`, `games`, or `textures`)
5. Git clone there 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 **ContentDB does not allow certain non-free licenses, and will limit the promotion
of packages with non-free licenses.** 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 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 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 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 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. 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 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 will be subject to limited promotion - they won't be shown by default in
the client. the client.
@@ -37,7 +37,7 @@ you spread it.
## What's so bad about licenses that forbid commercial use? ## 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). 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 1. They make your work incompatible with a growing body of free content, even if
you do want to allow derivative works or combinations. 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 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 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) leaving it blank. See [Content Warnings](/help/content_flags/#content-warnings)
for information on mature content. 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: 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 ?response_type=code
&client_id={CLIENT_ID} &client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URL} &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: Do this by making a POST request to the `/oauth/token/` API:
```bash ```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 grant_type=authorization_code \
-F client_id="CLIENT_ID" \ -F client_id="CLIENT_ID" \
-F client_secret="CLIENT_SECRET" \ -F client_secret="CLIENT_SECRET" \
@@ -98,6 +98,6 @@ Possible errors:
Next, you should check the access token works by getting the user information: Next, you should check the access token works by getting the user information:
```bash ```bash
curl https://content.minetest.net/api/whoami/ \ curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN" -H "Authorization: Bearer YOURTOKEN"
``` ```

View File

@@ -42,8 +42,8 @@ ContentDB understands the following information:
* `description` - A short description to show in the client. * `description` - A short description to show in the client.
* `depends` - Comma-separated hard dependencies. * `depends` - Comma-separated hard dependencies.
* `optional_depends` - Comma-separated soft 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). * `min_minetest_version` - The minimum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
* `max_minetest_version` - The maximum Minetest version this runs on, see [Min and Max Minetest 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: 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/). * `tags`: List of tag names, see [/api/tags/](/api/tags/).
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/). * `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
* `license`: A license name, see [/api/licenses/](/api/licenses/). * `license`: A license name, see [/api/licenses/](/api/licenses/).
* `media_license`: A license name. * `media_license`: A license name.
* `long_description`: Long markdown description. * `long_description`: Long markdown description.
* `repo`: Source repository (eg: Git). * `repo`: Source repository (eg: Git).
* `website`: Website URL. * `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/) You can also use [GitLab/GitHub webhooks](/help/release_webhooks/) or the [API](/help/api/)
to create releases. to create releases.
### Min and Max Minetest Versions ### Min and Max Luanti Versions
<a name="min_max_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 versions the release supports. If the `.conf` doesn't specify, then it is assumed
that it supports all versions. 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. 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. 4. ContentDB checks the API token and issues a new release.
* If multiple packages match, then only the first will have a release created. * If multiple packages match, then only the first will have a release created.
### Branch filtering ### Branch filtering
By default, "New commit" or "push" based webhooks will only work on "master"/"main" branches. 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/). 1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated. 2. Copy the access token that was generated.
3. Go to the GitLab repository's settings > Webhooks > Add Webhook. 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. 5. Set the content type to JSON.
6. Set the secret to the access token that you copied. 6. Set the secret to the access token that you copied.
7. Set the events 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/). 1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated. 2. Copy the access token that was generated.
3. Go to the GitLab repository's settings > Webhooks. 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. 6. Set the secret token to the ContentDB access token that you copied.
7. Set the events 7. Set the events
* If you want a rolling release, choose "Push 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 See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation. 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. 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 You can see all scores using the [scores REST API](/api/scores/), or by
using the [Prometheus metrics](/help/metrics/) endpoint. 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 See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation. 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. and update the package meta.

View File

@@ -1,25 +1,6 @@
title: WTFPL is a terrible license title: WTFPL is a terrible license
toc: False 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. 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> * **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) 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) 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 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 ## 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. Accepted Content
### 2.1. Acceptable Content ### 2.1. Mature Content
Sexually-orientated content is not permitted. See the [Terms of Service](/terms/) for a full list of prohibited content.
If in doubt at what this means, [contact us by raising a report](/report/).
Mature content is permitted providing that it is labelled correctly. Other mature content is permitted providing that it is labelled with the applicable
See [Content Flags](/help/content_flags/). [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 ContentDB is for playable and useful content - content which is sufficiently
sufficiently complete to be useful to end-users. It's fine to add stuff which is complete to be useful to end-users.
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.
You should make sure to mark Work in Progress stuff as such in the "maintenance It's fine to add stuff which is still a Work in Progress (WIP) as long as it
status" column, as this will help advise players. 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 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 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. 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. 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. 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. 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 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 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 We reserve the right to decide whether a mod counts as a fork or
reimplementation of the mod that owns the name. 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. 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) 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 that you have used in your package.
the [Copyright help page](/help/copyright/).
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 **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. 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. 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 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 get around to adding it. We reject custom/untested licenses and reserve the right
reserve the right to decide whether a license should be included. to decide whether a license should be included.
Please note that the definitions of "free" and "non-free" is the same as that 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). 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) 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 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 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. on [non-free licenses](/help/non_free/) for more information.
It is recommended that you use a proper license for code with a warranty 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 ## 7. Screenshots
1. **Screenshots must not violate copyright.** You should have the rights to the 1. We require all packages to have at least one screenshot. For packages without visual
screenshot. 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.** not be misleading.**
Do not use idealized mockups or blender concept renders if they do not 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 will look like in a typical/realistic game scene, but should be "in the
background" only as far as possible. 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.** 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 ## 8. Security

View File

@@ -2,7 +2,7 @@ title: Privacy Policy
--- ---
Last Updated: 2024-04-30 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 ## 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. * 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. 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, The keys and the backups themselves are given to different people,
requiring at least two staff members to read a backup. requiring at least two staff members to read a backup.
* Email addresses are visible to moderators and the admin. * 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))) (end_date is None or entry.created_at <= end_date)))
info.is_in_range = info.is_in_range or is_in_range 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: if new_state == info.state:
continue continue

View File

@@ -174,12 +174,6 @@ class GameSupport:
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]: def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
if package.id_ in visited: 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 return None
if package.type == PackageType.GAME: if package.type == PackageType.GAME:

View File

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

View File

@@ -69,6 +69,19 @@ ALLOWED_FIELDS = {
"translation_url": str, "translation_url": str,
} }
NULLABLE = {
"tags",
"content_warnings",
"repo",
"website",
"issue_tracker",
"issueTracker",
"forums",
"video_url",
"donate_url",
"translation_url",
}
ALIASES = { ALIASES = {
"short_description": "short_desc", "short_description": "short_desc",
"issue_tracker": "issueTracker", "issue_tracker": "issueTracker",
@@ -86,11 +99,13 @@ def is_int(val):
def validate(data: dict): def validate(data: dict):
for key, value in data.items(): 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) 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: 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: if "name" in data:
name = data["name"] name = data["name"]
@@ -102,12 +117,12 @@ def validate(data: dict):
value = data.get(key) value = data.get(key)
if value is not None: if value is not None:
check(value.startswith("http://") or value.startswith("https://"), check(value.startswith("http://") or value.startswith("https://"),
key + " must start with http:// or https://") f"{key} must start with http:// or https://")
check(validators.url(value), key + " must be a valid URL") 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, 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): if not package.check_perm(user, Permission.EDIT_PACKAGE):
raise LogicError(403, lazy_gettext("You don't have permission to edit this 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(): for alias, to in ALIASES.items():
if alias in data: 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] data[to] = data[alias]
validate(data) validate(data)
@@ -169,7 +187,6 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
package.provides.append(m) package.provides.append(m)
if "tags" in data: if "tags" in data:
old_tags = list(package.tags)
package.tags.clear() package.tags.clear()
for tag_id in (data["tags"] or []): for tag_id in (data["tags"] or []):
if is_int(tag_id): 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) raise LogicError(400, "Unknown warning: " + warning_id)
package.content_warnings.append(warning) 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("/") after_dict = package.as_dict("/")
diff = diff_dictionaries(before_dict, after_dict) diff = diff_dictionaries(before_dict, after_dict)
was_modified = len(diff) > 0
if reason is None: if reason is None:
msg = "Edited {}".format(package.title) 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 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)) 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.LogicError import LogicError
from app.logic.uploads import upload_file 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.tasks.importtasks import make_vcs_release, check_zip_release
from app.utils import AuditSeverity, add_audit_log, nonempty_or_none, normalize_line_endings 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): if not package.check_perm(user, Permission.MAKE_RELEASE):
raise LogicError(403, lazy_gettext("You don't have permission to make releases")) 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: 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")) 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, 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): min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None):
check_can_create_release(user, package) check_can_create_release(user, package, name)
rel = PackageRelease() rel = PackageRelease()
rel.package = package 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, 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): commit_hash: str = None):
check_can_create_release(user, package) check_can_create_release(user, package, name)
if commit_hash: if commit_hash:
commit_hash = commit_hash.lower() commit_hash = commit_hash.lower()

View File

@@ -17,7 +17,7 @@
import imghdr import imghdr
import os import os
from flask_babel import lazy_gettext from flask_babel import lazy_gettext, LazyString
from app import app from app import app
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
@@ -35,7 +35,7 @@ def is_allowed_image(data):
return imghdr.what(None, data) in ALLOWED_IMAGES 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 == "": if not file or file is None or file.filename == "":
raise LogicError(400, "Expected file") raise LogicError(400, "Expected file")
@@ -62,7 +62,7 @@ def upload_file(file, file_type, file_type_desc):
file.stream.seek(0) file.stream.seek(0)
filename = random_string(10) + "." + ext filename = random_string(length) + "." + ext
filepath = os.path.join(app.config["UPLOAD_DIR"], filename) filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
file.save(filepath) 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 # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask_babel import LazyString
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_searchable import make_searchable from sqlalchemy_searchable import make_searchable
@@ -125,13 +124,115 @@ class AuditLogEntry(db.Model):
raise Exception("Unknown permission given to AuditLogEntry.check_perm()") raise Exception("Unknown permission given to AuditLogEntry.check_perm()")
if perm == Permission.VIEW_AUDIT_DESCRIPTION: 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: else:
raise Exception("Permission {} is not related to audit log entries".format(perm.name)) 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", 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", "digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
"imageshack.com", "imgur.com"] "imageshack.com", "imgur.com"]
@@ -158,7 +259,7 @@ class ForumTopic(db.Model):
@property @property
def url(self): 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): def get_repo_url(self):
if self.link is None: if self.link is None:

View File

@@ -457,7 +457,7 @@ class Package(db.Model):
if self.forums is None: if self.forums is None:
return 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) enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True)
@@ -679,6 +679,7 @@ class Package(db.Model):
"website": self.website, "website": self.website,
"issue_tracker": self.issueTracker, "issue_tracker": self.issueTracker,
"forums": self.forums, "forums": self.forums,
"forum_url": self.forums_url,
"video_url": self.video_url, "video_url": self.video_url,
"video_thumbnail_url": self.get_video_thumbnail_url(True), "video_thumbnail_url": self.get_video_thumbnail_url(True),
"donate_url": self.donate_url_actual, "donate_url": self.donate_url_actual,
@@ -811,7 +812,7 @@ class Package(db.Model):
elif perm == Permission.APPROVE_SCREENSHOT: elif perm == Permission.APPROVE_SCREENSHOT:
return (is_maintainer or is_approver) and \ 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: elif perm == Permission.EDIT_MAINTAINERS or perm == Permission.DELETE_PACKAGE:
return is_owner or user.rank.at_least(UserRank.EDITOR) 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) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False) name = db.Column(db.String(100), unique=True, nullable=False)
protocol = db.Column(db.Integer, nullable=False, default=0) protocol = db.Column(db.Integer, nullable=False, default=0)
@@ -1066,12 +1067,11 @@ class MinetestRelease(db.Model):
} }
@classmethod @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: if version:
parts = version.strip().split(".") parts = version.strip().split(".")
if len(parts) >= 2: if len(parts) >= 2:
major_minor = parts[0] + "." + parts[1] query = LuantiRelease.query.filter(func.replace(LuantiRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
if protocol_num: if protocol_num:
query = query.filter_by(protocol=protocol_num) query = query.filter_by(protocol=protocol_num)
@@ -1081,9 +1081,9 @@ class MinetestRelease(db.Model):
if protocol_num: if protocol_num:
# Find the closest matching release # Find the closest matching release
return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol), return LuantiRelease.query.order_by(db.desc(LuantiRelease.protocol),
db.desc(MinetestRelease.id)) \ db.desc(LuantiRelease.id)) \
.filter(MinetestRelease.protocol <= protocol_num).first() .filter(LuantiRelease.protocol <= protocol_num).first()
return None return None
@@ -1103,6 +1103,7 @@ class PackageRelease(db.Model):
commit_hash = db.Column(db.String(41), nullable=True, default=None) commit_hash = db.Column(db.String(41), nullable=True, default=None)
downloads = db.Column(db.Integer, nullable=False, default=0) downloads = db.Column(db.Integer, nullable=False, default=0)
release_notes = db.Column(db.UnicodeText, nullable=True, default=None) release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
@property @property
def summary(self) -> str: def summary(self) -> str:
@@ -1113,11 +1114,11 @@ class PackageRelease(db.Model):
return self.release_notes.split("\n")[0] 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_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id]) 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_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id]) 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 # 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)") 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): def file_path(self):
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"]) return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
@property def calculate_file_size_bytes(self):
def file_size_bytes(self):
path = self.file_path path = self.file_path
if not os.path.isfile(path): if not os.path.isfile(path):
return 0 self.file_size_bytes = 0
return
file_stats = os.stat(path) file_stats = os.stat(path)
return file_stats.st_size self.file_size_bytes = file_stats.st_size
@property @property
def file_size(self): def file_size(self):
@@ -1263,6 +1264,8 @@ class PackageScreenshot(db.Model):
width = db.Column(db.Integer, nullable=False) width = db.Column(db.Integer, nullable=False)
height = 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): def is_very_small(self):
return self.width < 720 or self.height < 405 return self.width < 720 or self.height < 405
@@ -1276,14 +1279,14 @@ class PackageScreenshot(db.Model):
def file_path(self): def file_path(self):
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"]) return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
@property def calculate_file_size_bytes(self):
def file_size_bytes(self):
path = self.file_path path = self.file_path
if not os.path.isfile(path): if not os.path.isfile(path):
return 0 self.file_size_bytes = 0
return
file_stats = os.stat(path) file_stats = os.stat(path)
return file_stats.st_size self.file_size_bytes = file_stats.st_size
@property @property
def file_size(self): 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 # 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) 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) trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT)
ref = db.Column(db.String(41), nullable=True, default=None) 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) reason_update = db.Column(db.Integer, nullable=False, default=0)
@staticmethod @staticmethod
def update(package: Package, is_minetest: bool, reason: str): def update(package: Package, is_luanti: bool, reason: str):
date = datetime.datetime.utcnow().date() date = datetime.datetime.utcnow().date()
to_update = dict() to_update = dict()
@@ -1446,7 +1451,7 @@ class PackageDailyStats(db.Model):
"package_id": package.id, "date": date "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 to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1
kwargs[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") 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", first_reply = db.relationship("ThreadReply", uselist=False, foreign_keys="ThreadReply.thread_id",
lazy=True, order_by=db.asc("id"), viewonly=True, lazy=True, order_by=db.asc("id"), viewonly=True,
primaryjoin="Thread.id==ThreadReply.thread_id") primaryjoin="Thread.id==ThreadReply.thread_id")

View File

@@ -96,6 +96,7 @@ class Permission(enum.Enum):
CHANGE_USERNAMES = "CHANGE_USERNAMES" CHANGE_USERNAMES = "CHANGE_USERNAMES"
CHANGE_RANK = "CHANGE_RANK" CHANGE_RANK = "CHANGE_RANK"
CHANGE_EMAIL = "CHANGE_EMAIL" CHANGE_EMAIL = "CHANGE_EMAIL"
LINK_TO_WEBSITE = "LINK_TO_WEBSITE"
SEE_THREAD = "SEE_THREAD" SEE_THREAD = "SEE_THREAD"
CREATE_THREAD = "CREATE_THREAD" CREATE_THREAD = "CREATE_THREAD"
COMMENT_THREAD = "COMMENT_THREAD" COMMENT_THREAD = "COMMENT_THREAD"
@@ -114,6 +115,7 @@ class Permission(enum.Enum):
EDIT_COLLECTION = "EDIT_COLLECTION" EDIT_COLLECTION = "EDIT_COLLECTION"
VIEW_COLLECTION = "VIEW_COLLECTION" VIEW_COLLECTION = "VIEW_COLLECTION"
CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT" CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT"
SEE_REPORT = "SEE_REPORT"
# Only return true if the permission is valid for *all* contexts # Only return true if the permission is valid for *all* contexts
# See Package.check_perm for package-specific 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") 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")) 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") 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) 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" return "/static/bot_avatar.png"
else: else:
from app.utils.gravatar import get_gravatar 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): def check_perm(self, user, perm):
if not user.is_authenticated: if not user.is_authenticated:
@@ -287,6 +290,8 @@ class User(db.Model, UserMixin):
return user.rank.at_least(UserRank.NEW_MEMBER) return user.rank.at_least(UserRank.NEW_MEMBER)
else: else:
return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank) 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: else:
raise Exception("Permission {} is not related to users".format(perm.name)) 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 = { const data = {
datasets: [ datasets: [
{ label: "Web / other", data: getData(json.platform_other) }, { 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); setup_chart(ctx, data, annotations);

View File

@@ -26,7 +26,7 @@ window.addEventListener("load", () => {
try { try {
const pasteData = e.clipboardData.getData('text'); const pasteData = e.clipboardData.getData('text');
const url = new URL(pasteData); const url = new URL(pasteData);
if (url.hostname === "forum.minetest.net") { if (url.hostname === "forum.luanti.org") {
forumsField.value = url.searchParams.get("t"); forumsField.value = url.searchParams.get("t");
e.preventDefault(); e.preventDefault();
} }
@@ -37,7 +37,7 @@ window.addEventListener("load", () => {
const openForums = document.getElementById("forums-button"); const openForums = document.getElementById("forums-button");
openForums.addEventListener("click", () => { 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) { function setupHints(id, hints) {
@@ -68,8 +68,9 @@ window.addEventListener("load", () => {
} }
setupHints("short_desc", { setupHints("short_desc", {
"short_desc_mods": (val) => val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 || "short_desc_mods": (val) => val.indexOf("luanti") >= 0 || val.indexOf("minetest") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0, val.indexOf("mod") >= 0 || val.indexOf("modpack") >= 0 ||
val.indexOf("mod pack") >= 0,
}); });
setupHints("desc", { setupHints("desc", {
@@ -85,7 +86,8 @@ window.addEventListener("load", () => {
"desc_page_topic": (val) => { "desc_page_topic": (val) => {
const topicId = document.getElementById("forums").value; const topicId = document.getElementById("forums").value;
const r = new RegExp(`forum\\.minetest\\.net\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`); 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) => { "desc_page_repo": (val) => {
const repoUrl = document.getElementById("repo").value.replace(".git", ""); 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; let tries = 0;
while (true) { while (true) {
@@ -42,6 +42,10 @@ async function pollTask(poll_url, disableTimeout) {
console.error(e); console.error(e);
} }
if (res && res.status) {
onProgress?.(res);
}
if (res && res.status === "SUCCESS") { if (res && res.status === "SUCCESS") {
console.log("Got result") console.log("Got result")
return res.result; return res.result;
@@ -62,3 +66,41 @@ async function performTask(url) {
throw "Start task didn't return string!"; 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> <ShortName>ContentDB</ShortName>
<LongName>ContentDB</LongName> <LongName>ContentDB</LongName>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<Description>Search mods, games, and textures for Minetest.</Description> <Description>Search mods, games, and textures for Luanti.</Description>
<Tags>Minetest Mod Game Subgame Search</Tags> <Tags>Luanti Minetest Mod Game Subgame Search</Tags>
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/> <Url type="text/html" method="get" template="https://content.luanti.org/packages?q={searchTerms}"/>
</OpenSearchDescription> </OpenSearchDescription>

View File

@@ -22,7 +22,7 @@ from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from sqlalchemy_searchable import search 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 ContentWarning, PackageState, PackageDevState
from .utils import is_yes, get_int_or_abort from .utils import is_yes, get_int_or_abort
@@ -49,7 +49,7 @@ class QueryBuilder:
hide_wip: bool hide_wip: bool
hide_nonfree: bool hide_nonfree: bool
show_added: bool show_added: bool
version: Optional[MinetestRelease] version: Optional[LuantiRelease]
has_lang: Optional[str] has_lang: Optional[str]
@property @property
@@ -163,12 +163,12 @@ class QueryBuilder:
self.author = args.get("author") self.author = args.get("author")
protocol_version = get_int_or_abort(args.get("protocol_version")) protocol_version = get_int_or_abort(args.get("protocol_version"))
minetest_version = args.get("engine_version") engine_version = args.get("engine_version")
if minetest_version == "": if engine_version == "":
minetest_version = None engine_version = None
if protocol_version or minetest_version: if protocol_version or engine_version:
self.version = MinetestRelease.get(minetest_version, protocol_version) self.version = LuantiRelease.get(engine_version, protocol_version)
else: else:
self.version = None self.version = None

View File

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

View File

@@ -51,6 +51,26 @@ h3 {
letter-spacing: .05em 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 { .badge-notify {
background:yellow; /* #00bc8c;*/ background:yellow; /* #00bc8c;*/
color: black; color: black;

View File

@@ -92,3 +92,12 @@
max-height: 1em; max-height: 1em;
filter: none !important; 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): def make_celery(app):
celery = FlaskCelery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], 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) celery.init_app(app)
return celery 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): def check_forum_account(forums_username, force_replace_pic=False):
print("### Checking " + forums_username, file=sys.stderr) print("### Checking " + forums_username, file=sys.stderr)
try: try:
profile = get_profile("https://forum.minetest.net", forums_username) profile = get_profile("https://forum.luanti.org", forums_username)
except OSError as e: except OSError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
return return
@@ -88,13 +88,13 @@ def check_forum_account(forums_username, force_replace_pic=False):
db.session.commit() db.session.commit()
if pic: 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"####### Picture: {pic}", file=sys.stderr)
print(f"####### User pp {user.profile_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 \ 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 user.profile_pic.startswith("https://forum.luanti.org") or force_replace_pic
if pic_needs_replacing and pic.startswith("https://forum.minetest.net"): if pic_needs_replacing and pic.startswith("https://forum.luanti.org"):
print(f"####### Queueing", file=sys.stderr) print(f"####### Queueing", file=sys.stderr)
set_profile_picture_from_url.delay(user.username, pic) set_profile_picture_from_url.delay(user.username, pic)

View File

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

View File

@@ -17,7 +17,7 @@
from enum import Enum from enum import Enum
class MinetestCheckError(Exception): class LuantiCheckError(Exception):
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
@@ -43,14 +43,14 @@ class ContentType(Enum):
if self == ContentType.MOD: if self == ContentType.MOD:
if not other.is_mod_like(): 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: elif self == ContentType.TXP:
if other != ContentType.UNKNOWN and other != 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: 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 from .tree import PackageTreeNode, get_base_dir

View File

@@ -20,12 +20,12 @@ import re
import glob import glob
from typing import Optional from typing import Optional
from . import MinetestCheckError, ContentType from . import LuantiCheckError, ContentType
from .config import parse_conf from .config import parse_conf
from .translation import Translation, parse_tr from .translation import Translation, parse_tr
basenamePattern = re.compile("^([a-z0-9_]+)$") 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 = { DISALLOWED_NAMES = {
"core", "minetest", "group", "table", "string", "lua", "luajit", "assert", "debug", "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: if dep == "*" and allow_star:
continue continue
elif " " in dep: elif " " in dep:
raise MinetestCheckError( raise LuantiCheckError(
f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?") f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?")
else: else:
raise MinetestCheckError( raise LuantiCheckError(
f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.") f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.")
@@ -90,6 +90,8 @@ class PackageTreeNode:
children: list children: list
type: ContentType type: ContentType
strict: bool strict: bool
has_legacy_depends: bool
has_legacy_description: bool
def __init__(self, base_dir: str, relative: str, def __init__(self, base_dir: str, relative: str,
author: Optional[str] = None, author: Optional[str] = None,
@@ -103,6 +105,8 @@ class PackageTreeNode:
self.meta = {} self.meta = {}
self.children = [] self.children = []
self.strict = strict self.strict = strict
self.has_legacy_depends = False
self.has_legacy_description = False
# Detect type # Detect type
self.type = detect_type(base_dir) self.type = detect_type(base_dir)
@@ -110,14 +114,14 @@ class PackageTreeNode:
if self.type == ContentType.GAME: if self.type == ContentType.GAME:
if not os.path.isdir(os.path.join(base_dir, "mods")): 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") self._add_children_from_mod_dir("mods")
elif self.type == ContentType.MOD: elif self.type == ContentType.MOD:
if self.name and not basenamePattern.match(self.name): 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: 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"]) self._check_dir_casing(["textures", "media", "sounds", "models", "locale"])
elif self.type == ContentType.MODPACK: elif self.type == ContentType.MODPACK:
@@ -135,7 +139,7 @@ class PackageTreeNode:
for dir in next(os.walk(self.baseDir))[1]: for dir in next(os.walk(self.baseDir))[1]:
lowercase = dir.lower() lowercase = dir.lower()
if lowercase != dir and lowercase in dirs: 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): def get_readme_path(self):
for filename in os.listdir(self.baseDir): for filename in os.listdir(self.baseDir):
@@ -169,12 +173,12 @@ class PackageTreeNode:
for key, value in conf.items(): for key, value in conf.items():
result[key] = value result[key] = value
except SyntaxError as e: 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: except IOError:
pass pass
if self.strict and "release" in result: 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 # description.txt
if "description" not in result: if "description" not in result:
@@ -184,6 +188,11 @@ class PackageTreeNode:
except IOError: except IOError:
pass 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 # Read dependencies
if "depends" in result or "optional_depends" in result: if "depends" in result or "optional_depends" in result:
result["depends"] = get_csv_line(result.get("depends")) result["depends"] = get_csv_line(result.get("depends"))
@@ -235,9 +244,12 @@ class PackageTreeNode:
# Calculate short description # Calculate short description
if "description" in result: if "description" in result:
desc = result["description"] desc = result["description"]
idx = desc.find(".") + 1 if len(desc) > 200:
cutIdx = min(len(desc), 200 if idx < 5 else idx) idx = desc.find(".") + 1
result["short_description"] = desc[:cutIdx] idx = min(len(desc), 200 if idx < 5 else idx)
result["short_description"] = desc[:idx]
else:
result["short_description"] = desc
if "name" in result: if "name" in result:
self.name = result["name"] self.name = result["name"]
@@ -257,11 +269,11 @@ class PackageTreeNode:
if not entry.startswith('.') and os.path.isdir(path): if not entry.startswith('.') and os.path.isdir(path):
child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict) child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict)
if not child.type.is_mod_like(): 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)) .format(child.type.value, child.relative, self.type.value))
if child.name is None: 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) self.children.append(child)
@@ -301,6 +313,16 @@ class PackageTreeNode:
def get(self, key: str, default=None): def get(self, key: str, default=None):
return self.meta.get(key, default) 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): def validate(self):
for child in self.children: for child in self.children:
child.validate() child.validate()
@@ -313,14 +335,19 @@ class PackageTreeNode:
return ret return ret
def get_translations(self, textdomain: str) -> list[Translation]: def get_translations(self, textdomain: str, allowed_languages: set[str]) -> list[Translation]:
ret = [] ret = []
for name in glob.glob(f"{self.baseDir}/**/locale/{textdomain}.*.tr", recursive=True): 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: try:
ret.append(parse_tr(name)) ret.append(parse_tr(name))
except SyntaxError as e: except SyntaxError as e:
relative_path = os.path.join(self.relative, os.path.relpath(name, self.baseDir)) 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 return ret

View File

@@ -19,15 +19,16 @@ import random
import re import re
import sys import sys
from time import sleep from time import sleep
from urllib.parse import urlparse from urllib.parse import urlparse, urljoin
from typing import Optional from typing import Optional
import requests import requests
import urllib3 import urllib3
from app import app
from sqlalchemy import or_, and_ from sqlalchemy import or_, and_
from app.markdown import get_links, render_markdown 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.tasks import celery, TaskError
from app.utils import post_bot_message, post_to_approval_thread, get_system_user, add_audit_log 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): 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("%", ".*")) reg = re.compile(search_str.replace(".", "\\.").replace("/", "\\/").replace("?", "\\?").replace("%", ".*"))
return reg.search(desc) return reg.search(desc)
else: else:
@@ -57,7 +58,7 @@ def notify_about_git_forum_links():
.filter(Package.repo.is_not(None), Package.state == PackageState.APPROVED).all()] .filter(Package.repo.is_not(None), Package.state == PackageState.APPROVED).all()]
for pair in db.session.query(Package, Package.forums) \ for pair in db.session.query(Package, Package.forums) \
.filter(Package.forums.is_not(None), Package.state == PackageState.APPROVED).all(): .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] 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() 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: def _url_exists(url: str) -> str:
try: try:
headers = { 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: with requests.get(url, stream=True, headers=headers, timeout=10) as response:
response.raise_for_status() response.raise_for_status()
return "" return ""
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
return ""
print(f" - [{e.response.status_code}] <{url}>", file=sys.stderr) print(f" - [{e.response.status_code}] <{url}>", file=sys.stderr)
return str(e.response.status_code) return str(e.response.status_code)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
@@ -125,6 +129,9 @@ def _url_exists(url: str) -> str:
def _check_for_dead_links(package: Package) -> dict[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]] = { links: set[Optional[str]] = {
package.repo, package.repo,
package.website, package.website,
@@ -136,7 +143,7 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
} }
if package.desc: 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) 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: if link is None:
continue continue
url = urlparse(link) abs_link = urljoin(base_url, link)
url = urlparse(abs_link)
if url.scheme != "http" and url.scheme != "https": if url.scheme != "http" and url.scheme != "https":
continue continue
res = _url_exists(link) if url.hostname in ignored_urls:
continue
res = _url_exists(abs_link)
if res != "": if res != "":
bad_urls[link] = res bad_urls[link] = res
@@ -180,7 +191,7 @@ def check_package_on_submit(package_id: int):
msg = _check_package(package) msg = _check_package(package)
if msg: 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() system_user = get_system_user()
post_to_approval_thread(package, system_user, marked, is_status_update=True, create_thread=True) 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: if msg:
post_bot_message(package, "Broken links", msg) post_bot_message(package, "Broken links", msg)
db.session.commit() 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 import sys
from flask import url_for from flask import url_for
from sqlalchemy import or_, and_ from sqlalchemy import or_, and_, not_, func
from app import app from app import app
from app.models import User, db, UserRank, ThreadReply, Package, NotificationType from app.models import User, db, UserRank, ThreadReply, Package, NotificationType
@@ -149,3 +149,37 @@ def import_github_user_ids():
db.session.commit() db.session.commit()
print(f"Updated {count} users", file=sys.stderr) 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() @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): 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") discord_urls = app.config.get("DISCORD_WEBHOOK_QUEUE" if is_queue else "DISCORD_WEBHOOK_FEED")
if discord_url is None: if discord_urls is None:
return return
if isinstance(discord_urls, str):
discord_urls = [discord_urls]
json = { json = {
"content": content[0:2000], "content": content[0:2000],
} }
@@ -52,7 +55,8 @@ def post_discord_webhook(username: Optional[str], content: str, is_queue: bool,
json["embeds"] = [embed] json["embeds"] = [embed]
res = requests.post(discord_url, json=json, headers={"Accept": "application/json"}) for url in discord_urls:
if not res.ok: res = requests.post(url, json=json, headers={"Accept": "application/json"})
raise Exception(f"Failed to submit Discord webhook {res.json}") if not res.ok:
res.raise_for_status() 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import subprocess import subprocess
from subprocess import Popen, PIPE import sys
from typing import Optional from subprocess import Popen, PIPE, TimeoutExpired
from typing import Optional, List
from app.models import Package, PackageState, PackageRelease from app.models import Package, PackageState, PackageRelease
from app.tasks import celery from app.tasks import celery
@celery.task() @celery.task(bind=True)
def search_in_releases(query: str, file_filter: str): def search_in_releases(self, query: str, file_filter: str, types: List[str]):
packages = list(Package.query.filter(Package.state == PackageState.APPROVED).all()) pkg_query = Package.query.filter(Package.state == PackageState.APPROVED)
running = [] if len(types) > 0:
pkg_query = pkg_query.filter(Package.type.in_(types))
packages = list(pkg_query.all())
results = [] results = []
while len(packages) > 0 or len(running) > 0: total = len(packages)
# Check running self.update_state(state="PROGRESS", meta={"current": 0, "total": total})
for i in range(len(running) - 1, -1, -1):
package: Package = running[i][0] while len(packages) > 0:
handle: subprocess.Popen[str] = running[i][1] 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() exit_code = handle.poll()
if exit_code is None: 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: elif exit_code == 0:
print(f"[Zipgrep] Success for {package.name}", file=sys.stderr)
results.append({ results.append({
"package": package.as_key_dict(), "package": package.as_key_dict(),
"lines": handle.stdout.read(), "lines": handle.stdout.read(),
}) })
elif exit_code != 1:
del running[i] print(f"[Zipgrep] Error {exit_code} for {package.name}", file=sys.stderr)
results.append({
# Create new "package": package.as_key_dict(),
while len(running) < 1 and len(packages) > 0: "lines": f"Error: exit {exit_code}",
package = packages.pop() })
release: Optional[PackageRelease] = package.get_download_release()
if release:
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
running.append([package, handle])
if len(running) > 0:
running[0][1].wait()
return { return {
"query": query, "query": query,

View File

@@ -23,10 +23,10 @@ from flask_login import current_user
from markupsafe import Markup from markupsafe import Markup
from . import app, utils from . import app, utils
from .markdown import get_headings from app.markdown import get_headings
from .models import Permission, Package, PackageState, PackageRelease from .models import Permission, Package, PackageState, PackageRelease
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current 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 @app.context_processor

View File

@@ -7,6 +7,14 @@ Audit Log
{% block content %} {% block content %}
<h1>Audit Log</h1> <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/pagination.html" import render_pagination %}
{% from "macros/audit_log.html" import render_audit_log %} {% from "macros/audit_log.html" import render_audit_log %}

View File

@@ -13,14 +13,20 @@
<div class="list-group"> <div class="list-group">
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}"> <a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
User list {{ _("User list") }}
</a> </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') }}"> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.audit') }}">
<i class="fas fa-user-clock me-2"></i> <i class="fas fa-user-clock me-2"></i>
{{ _("Audit Log") }} {{ _("Audit Log") }}
</a> </a>
{% endif %} {% 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> </div>
<h3>Packages</h3> <h3>Packages</h3>

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
{%- endif %} {%- endif %}
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4"> <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" /> <link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
{% if noindex -%} {% if noindex -%}
@@ -252,14 +252,14 @@
<footer class="my-5 pt-5"> <footer class="my-5 pt-5">
<p class="pt-3 mb-1"> <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> </p>
<ul class="list-inline my-1"> <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='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='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='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('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('donate.donate') }}#contentdb">{{ _("Donate") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/api') }}">{{ _("API") }}</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('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('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="{{ 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> </ul>
<form method="POST" action="{{ url_for('set_nonfree') }}" class="my-3"> <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') }}"> <input type="submit" class="btn btn-sm btn-secondary" value="{{ _('Hide non-free packages') }}">
{% endif %} {% endif %}
</form> </form>
<p class="text-warning"> {% if false %}
{{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }} <p class="text-warning">
</p> {{ _("Our privacy policy has been updated (%(date)s)", date="2024-04-30") }}
</p>
{% endif %}
{% if debug %} {% if debug %}
<p style="color: red"> <p style="color: red">

View File

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

View File

@@ -103,10 +103,10 @@
<h3 class="mt-5">{{ _("Downloads by Reason") }}</h3> <h3 class="mt-5">{{ _("Downloads by Reason") }}</h3>
<ul> <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>Dependency</b>: was installed automatically to fulfill a dependency.") }}</li>
<li>{{ _("<b>Update</b>: download was to update the package.") }}</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> </ul>
<p class="text-muted"> <p class="text-muted">
{{ _("This is a stacked area graph. For total downloads, look at the combined height.") }} {{ _("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" /> <input class="btn btn-primary" name="btn_submit" type="submit" value="Comment" />
</form> </form>
{% endif %} {% 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> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@
[{{ topic.type.text }}] [{{ topic.type.text }}]
</td> </td>
<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 %} {% if topic.wip %}[{{ _("WIP") }}]{% endif %}
</td> </td>
{% if show_author %} {% if show_author %}
@@ -42,7 +42,7 @@
{% macro render_topics(topics, current_user) -%} {% macro render_topics(topics, current_user) -%}
<div class="list-group"> <div class="list-group">
{% for topic in topics %} {% 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"> <span class="float-end text-muted">
{{ topic.created_at | date }} {{ topic.created_at | date }}
</span> </span>

View File

@@ -23,7 +23,7 @@
{% for t in similar_topics %} {% for t in similar_topics %}
<li> <li>
[{{ t.type.text }}] [{{ 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) }} {{ _("%(title)s by %(display_name)s", title=t.title, display_name=t.author.display_name) }}
</a> </a>
{% if t.wip %}[{{ _("WIP") }}]{% endif %} {% if t.wip %}[{{ _("WIP") }}]{% endif %}

View File

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