Compare commits

...

2060 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
rubenwardy
e31433f320 Remove outdated example from Package Inclusion Policy 2024-07-09 00:09:16 +01:00
rubenwardy
1f29938186 Add admin task to check all zip files 2024-07-08 21:33:22 +01:00
rubenwardy
a62f68ea5a Improve validation of zip files
Fixes #379
2024-07-08 20:46:05 +01:00
rubenwardy
25da8f5e21 Update SECURITY.md 2024-07-08 09:58:59 +01:00
rubenwardy
f588dc6cff Add audit log entry for CDB changing package state 2024-07-05 01:37:19 +01:00
rubenwardy
8605ee6fd8 Fix template error 2024-07-05 01:23:46 +01:00
rubenwardy
211be30cf4 Fix various things with broken link detection 2024-07-05 01:20:33 +01:00
rubenwardy
9bf91f17d6 Add full datetime as tooltips 2024-07-05 01:19:33 +01:00
rubenwardy
576d9dd3e0 Fix various issues with broken link checking 2024-07-04 22:57:26 +01:00
rubenwardy
b31268c9f2 Fix check_package_on_submit post issue 2024-07-04 22:07:39 +01:00
rubenwardy
894ed19556 Fix SQLAlchemy error by using user instead of current_user 2024-07-04 22:05:00 +01:00
rubenwardy
542e51e733 Fix SQLAlchemy error by using ids not mapped instances v2 2024-07-04 22:02:23 +01:00
rubenwardy
6a64f3f24d Fix SQLAlchemy error by using ids not mapped instances 2024-07-04 22:00:02 +01:00
rubenwardy
f81b7523d4 Run admin action on more than top 100 packages 2024-07-04 21:55:23 +01:00
rubenwardy
1006971271 Add admin action to check for broken links
Fixes #546
2024-07-04 21:52:24 +01:00
rubenwardy
d738e19ce9 Include status code in broken link messages 2024-07-04 21:35:37 +01:00
rubenwardy
3f62a41952 Check packages for broken links when submitting for approval
Half of #546
2024-07-04 21:29:56 +01:00
rubenwardy
a38a650dc1 Game Support: Clean up code a little 2024-07-04 20:25:48 +01:00
rubenwardy
813db2b8f9 Ignore merge commits in release notes generation 2024-07-04 20:24:19 +01:00
rubenwardy
59fad153ae Game Support: Remove caching as it causes obscure issues 2024-07-04 20:24:19 +01:00
rubenwardy
4302ba4bf2 Fix setting type to string value 2024-07-03 20:24:19 +01:00
rubenwardy
ed69a871a5 Prevent changing package type once approved
Fixes #547
2024-07-03 18:07:12 +01:00
rubenwardy
56f45510dd Feeds: Allow CORS 2024-07-03 17:58:42 +01:00
BlackImpostor
22926a69bd 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-03 00:30:55 +02:00
liu lizhi
9175f1b082 Translated using Weblate (Chinese (Simplified))
Currently translated at 79.4% (939 of 1182 strings)

Co-authored-by: liu lizhi <kz-xy@163.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-07-03 00:30:55 +02:00
Kisbenedek Márton
72829d6de6 Translated using Weblate (Hungarian)
Currently translated at 37.0% (438 of 1182 strings)

Co-authored-by: Kisbenedek Márton <martonkisbenedek@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2024-07-03 00:30:55 +02:00
Wuzzy
73c3863c1a Translated using Weblate (German)
Currently translated at 100.0% (1182 of 1182 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-07-03 00:30:55 +02:00
Olive
78a1c84d50 Added translation using Weblate (Toki Pona)
Co-authored-by: Olive <oliversimmo@gmail.com>
2024-07-03 00:30:55 +02:00
rubenwardy
aedeef4e02 Feeds: Fix sort order 2024-07-02 21:47:33 +01:00
rubenwardy
b741cc592f Feeds: Change release feed item titles 2024-07-02 21:43:32 +01:00
rubenwardy
5f1cd080bf Add Cache-Control to feeds 2024-07-02 21:36:42 +01:00
rubenwardy
d25dc2c795 Add Feeds help page 2024-07-02 21:30:23 +01:00
rubenwardy
31d5eb7e56 Add images to feeds 2024-07-02 21:25:25 +01:00
rubenwardy
19fa1d9b23 Fix JSONFeed version field
Unfortunately, the only working validator I found can only validate live URLs
2024-07-02 21:02:25 +01:00
rubenwardy
b4f9c99717 Fix missing content_html in JSONFeed
Unfortunately, the only working validator I found can only validate live URLs
2024-07-02 20:58:36 +01:00
rubenwardy
9062f49992 Fix authors in JSONFeed
Unfortunately, the only working validator I found can only validate live URLs
2024-07-02 20:55:01 +01:00
rubenwardy
f8abcaa7c6 Add Atom and JSON feeds for releases and new packages
Fixes #224
2024-07-02 20:41:39 +01:00
rubenwardy
e43a7827c2 Make Git Update Detection use release notes from annotated tag
Part of #545
2024-06-30 17:49:21 +01:00
rubenwardy
4ad8e3605b Use git log as fallback for release notes
Part of #545
2024-06-30 17:49:14 +01:00
rubenwardy
c21337b9ff Fix CI due to failing migration 2024-06-24 20:28:36 +01:00
rubenwardy
a0da9ef61e Add release notes to package view page 2024-06-24 20:25:25 +01:00
rubenwardy
22172da57e Fix migration when title is more than 30 chars 2024-06-24 20:16:17 +01:00
rubenwardy
a134d21b79 Remove final newline from normalize_line_endings
Database columns aren't files. This new line causes
issues with `!= ""` checks.
2024-06-24 20:10:03 +01:00
rubenwardy
d0fc83c00c Fix PackageRelease migration setting names to "title" 2024-06-24 20:03:04 +01:00
rubenwardy
64d8f30006 Fix crash when mentioning users in comments
Thanks @Dragonop for help finding the cause
2024-06-23 18:15:00 +01:00
rubenwardy
aecde93310 Fix issue when updating game support on multiple dependers 2024-06-23 11:14:10 +01:00
rubenwardy
0c4698ec0d Fix error on double "move_to_state" submission 2024-06-23 09:44:15 +01:00
rubenwardy
9a64ff7563 Fix missing name in release creation in two places 2024-06-23 09:42:31 +01:00
rubenwardy
1fc7aeb1dd Fix release notes length limit 2024-06-23 09:39:41 +01:00
rubenwardy
3f12a89764 Add fmt option to include VCS repo URL
Fixes #514
2024-06-22 17:15:52 +01:00
rubenwardy
232e3199fd Update translations 2024-06-22 16:06:40 +01:00
孙鑫然
c78c997817 Translated using Weblate (Chinese (Simplified))
Currently translated at 81.4% (941 of 1155 strings)

Co-authored-by: 孙鑫然 <sun_20120302@qq.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-06-22 17:05:59 +02:00
BlackImpostor
f8c032458e Translated using Weblate (Russian)
Currently translated at 100.0% (1155 of 1155 strings)

Translated using Weblate (Russian)

Currently translated at 88.2% (1019 of 1155 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-06-22 17:05:59 +02:00
rubenwardy
4cb7cc37f9 Hide failing releases from editor todo page 2024-06-22 15:57:23 +01:00
rubenwardy
23335f4d30 Make release_notes markdown 2024-06-22 15:48:54 +01:00
rubenwardy
44e6f42b51 Fix more failing integration tests 2024-06-22 15:42:12 +01:00
rubenwardy
37ff435ff3 Wrap example in API docs 2024-06-22 15:41:09 +01:00
rubenwardy
2f9a3f04b8 Fix failing integration test 2024-06-22 15:38:59 +01:00
rubenwardy
40edbc7a3b Add ability to set release name from API 2024-06-22 15:33:49 +01:00
rubenwardy
2d7845209f Add uncommitted migration for more release meta 2024-06-22 15:27:01 +01:00
rubenwardy
c06ca52f4c Fix setting release notes in release edit 2024-06-22 15:24:40 +01:00
rubenwardy
019cd66033 Add release notes and long titles to releases
Fixes #492 and fixes #480
2024-06-22 15:19:05 +01:00
rubenwardy
4147e5edc7 Improve claim_forums error messages 2024-06-22 14:35:26 +01:00
rubenwardy
71e68a6056 Fix engine version filtering breaking limit
Fixes #383
2024-06-22 14:23:22 +01:00
rubenwardy
8f453a8cdf Add list of pending email verifications to modtools 2024-06-22 14:02:28 +01:00
rubenwardy
04878fc9e0 Make API http errors return JSON
Fixes #384
2024-06-22 13:48:10 +01:00
rubenwardy
29a6a762cb Remove CSRF token expiry
According to the OWASP, CSRF tokens don't need expiry times. They should be bound to the session.

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-hmac-csrf-tokens

Fixes #437
2024-06-22 13:30:18 +01:00
rubenwardy
63ad6a2b9a Normalize long description line endings when set by API 2024-06-22 13:26:04 +01:00
rubenwardy
da090fd3f5 Normalize line endings in form submissions
Fixes #506
2024-06-22 13:22:37 +01:00
rubenwardy
d6e25f38a8 Fix Git integration test 2024-06-22 13:11:17 +01:00
rubenwardy
86ca3864a3 Fix EasyMDE and Bootstrap conflict 2024-06-22 13:08:26 +01:00
rubenwardy
6b5230b0c1 Update easymde to 2.18 2024-06-22 13:02:27 +01:00
rubenwardy
80888f0675 Fix tokens being deleted when package set to None 2024-06-22 12:57:08 +01:00
rubenwardy
b3c5824490 Make "Convert to Thread" for moderator reviews more obvious
Fixes #403
2024-06-22 12:44:43 +01:00
rubenwardy
7a94b9361f Allow filtering VCS webhooks based on branch name
Fixes #258
2024-06-22 12:13:49 +01:00
rubenwardy
09e06a159a Fix VCS webhooks assuming repo URLs are unique
Fixes #264
2024-06-22 12:01:09 +01:00
rubenwardy
ca961cb35f Fix various issues with forum topic importing
Fixes #201
2024-06-22 11:11:57 +01:00
rubenwardy
12545c69ac Add mismatched topics editor page 2024-06-22 10:45:14 +01:00
rubenwardy
aeca6cbbdb QueryBuilder: Update noindex 2024-06-11 21:45:17 +01:00
rubenwardy
211b130f98 Advanced Search: Use dropdown for supported game 2024-06-11 21:39:10 +01:00
rubenwardy
2c8b751f98 Advanced Search: Fix values, remove use of __None 2024-06-11 21:35:20 +01:00
rubenwardy
e75f2f92e2 Add advanced search interface
Fixes #112
2024-06-11 21:25:58 +01:00
rubenwardy
d5492cbb9b QueryBuilder: Allow hiding tags 2024-06-11 19:37:05 +01:00
rubenwardy
1a74471b68 QueryBuilder: Fix crash due to set changing size 2024-06-09 16:56:01 +01:00
rubenwardy
042e811a40 Fix tags sort by layout error 2024-06-09 16:53:09 +01:00
rubenwardy
7219c8b4a9 Remove link to deleted tags help page
Fixes #543
2024-06-09 16:50:49 +01:00
BlackImpostor
425420d663 Translated using Weblate (Russian)
Currently translated at 83.5% (965 of 1155 strings)

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

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-06-09 16:24:24 +02:00
rubenwardy
8b6bd8d282 Add gettext context to tags and warnings, update translations 2024-06-09 13:48:00 +01:00
rubenwardy
36644216b2 Fix some typos 2024-06-09 13:31:44 +01:00
rubenwardy
195008c69e Update translations 2024-06-09 13:28:45 +01:00
Wuzzy
8f8e68d3d3 Translated using Weblate (German)
Currently translated at 99.3% (1144 of 1152 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-06-09 14:26:48 +02:00
gallegonovato
f6a3f36f1a Translated using Weblate (Spanish)
Currently translated at 100.0% (1152 of 1152 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-06-09 14:26:48 +02:00
rubenwardy
80499dbf6c Fix remaining instances of package type as a gettext parameter
Fixes #355
2024-06-09 13:26:16 +01:00
rubenwardy
2869876b67 Fix Gettext metadata shown on empty tag description
Fixes #541
2024-06-09 13:22:07 +01:00
rubenwardy
5eb202941a Fix crash on users list page 2024-06-09 11:53:52 +01:00
rubenwardy
663fb38d9f Show review language on reviews 2024-06-09 11:50:21 +01:00
rubenwardy
b6e7e09171 Update translations 2024-06-09 11:40:25 +01:00
W T
aabbb693b2 Translated using Weblate (Polish)
Currently translated at 99.7% (1143 of 1146 strings)

Co-authored-by: W T <wiktor_t-i@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2024-06-09 12:38:30 +02:00
ROllerozxa
ba1523fc4b Translated using Weblate (Swedish)
Currently translated at 95.0% (1089 of 1146 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2024-06-09 12:38:30 +02:00
Wuzzy
c3ece9f102 Translated using Weblate (German)
Currently translated at 100.0% (1146 of 1146 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-06-09 12:38:30 +02:00
Joaquín Villalba
6883a079d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (1146 of 1146 strings)

Co-authored-by: Joaquín Villalba <joaco-mono@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-06-09 12:38:30 +02:00
gallegonovato
24310c920d Translated using Weblate (Spanish)
Currently translated at 100.0% (1146 of 1146 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-06-09 12:38:29 +02:00
SergioFLS
87c369998f Translated using Weblate (Spanish)
Currently translated at 100.0% (1146 of 1146 strings)

Co-authored-by: SergioFLS <sergioflsgd@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-06-09 12:38:29 +02:00
Lemente
dfad359290 Translated using Weblate (French)
Currently translated at 81.2% (931 of 1146 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-06-09 12:38:29 +02:00
rubenwardy
e335797629 Fix UserRank not being translatable 2024-06-08 12:27:05 +01:00
rubenwardy
7cf1f40ff6 Fix tags on spotlight carousel being untranslated 2024-06-08 12:16:52 +01:00
rubenwardy
a99a8a4df3 Update translations 2024-06-08 11:16:38 +01:00
Spectre (Nos)
94c26064cf Translated using Weblate (Arabic)
Currently translated at 4.7% (52 of 1094 strings)

Translated using Weblate (Arabic)

Currently translated at 4.2% (47 of 1094 strings)

Translated using Weblate (Arabic)

Currently translated at 4.2% (46 of 1094 strings)

Co-authored-by: Spectre (Nos) <bluesunnostromo2006@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ar/
Translation: Minetest/ContentDB
2024-06-08 12:14:33 +02:00
rubenwardy
3c096aac41 Add language to reviews 2024-06-08 11:12:42 +01:00
rubenwardy
f0039774e4 Fix remaining known untranslatable text
Fixes #351
2024-06-08 10:54:50 +01:00
rubenwardy
eb9466f346 Add separate translations for each content type
Fixes #355
Fixes #538
2024-06-08 10:46:47 +01:00
rubenwardy
a356a50abb Add mention of obfuscation to package policy 2024-06-07 23:05:24 +01:00
rubenwardy
598c02eeff Prompt users to set maintenance state rather than removing 2024-06-07 22:57:55 +01:00
ROllerozxa
22b1008593 Translated using Weblate (Swedish)
Currently translated at 100.0% (1094 of 1094 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2024-06-07 23:22:26 +02:00
gallegonovato
f6da62a606 Translated using Weblate (Spanish)
Currently translated at 100.0% (1094 of 1094 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-06-07 11:01:53 +02:00
rubenwardy
1c85e12f9e Add video_thumbnail_url to API 2024-06-07 06:32:22 +01:00
rubenwardy
5bd97598a8 Add YouTube thumbnail support
Fixes #359
2024-06-07 06:25:32 +01:00
rubenwardy
ee83a7b5ce Fix screenshots being distorted on collection pages
Fixes #497
2024-06-07 05:46:35 +01:00
rubenwardy
c731ab027a Update translations 2024-06-07 05:30:19 +01:00
BlackImpostor
86ee4d9caa Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-06-07 06:29:34 +02:00
John Donne
aadb98ed7c Translated using Weblate (French)
Currently translated at 97.9% (935 of 955 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-06-07 06:29:34 +02:00
rubenwardy
d2c5779301 Add ability to translate tags and content warnings 2024-06-07 05:28:57 +01:00
rubenwardy
7d00a5b969 Add list of possible licenses to error response 2024-06-05 19:45:37 +01:00
rubenwardy
804e131cb8 Fix case insensitive license search in querybuilder.py 2024-06-05 19:31:48 +01:00
rubenwardy
6a53f25665 Add prompt to read ContentDB's rules to review page 2024-06-05 19:28:16 +01:00
rubenwardy
380f009529 Add option to filter packages by license 2024-06-05 19:27:09 +01:00
rubenwardy
57ed2fc416 modtools: Redirect to tasks.check when changing GitHub username 2024-06-04 20:37:43 +01:00
rubenwardy
3b56ef7148 Add ability to filter audit log 2024-06-04 20:32:29 +01:00
rubenwardy
2653071886 Fix GitHub link not saved on GitHub-based registration 2024-06-04 20:29:46 +01:00
rubenwardy
5e122279ec Clean up user registration code 2024-06-02 21:24:21 +01:00
rubenwardy
4872ea9e6a Make GITHUB_API_TOKEN actually optional 2024-06-02 17:38:32 +01:00
rubenwardy
bb39f268d3 Fix potential issue with existing user query matching multiple users 2024-06-02 12:48:06 +01:00
rubenwardy
bce06d45d0 Allow signing up using GitHub 2024-06-02 12:46:56 +01:00
rubenwardy
54c50a815d Limit reason field length 2024-06-02 12:33:40 +01:00
rubenwardy
6b04324ee5 Limit text length sent to discord webhook 2024-06-02 12:29:49 +01:00
rubenwardy
8db31ebfa9 Add recalc package scores admin action 2024-06-02 12:26:22 +01:00
rubenwardy
1eaa5d8767 Add call-to-action to report outdated reviews 2024-06-02 12:25:54 +01:00
rubenwardy
522f12356a Add "Ask a question" button to create a thread 2024-06-02 12:20:07 +01:00
rubenwardy
e344e28166 Fix missing uncommited import 2024-06-02 12:10:55 +01:00
rubenwardy
2d29fb1994 Remove package deletion to worker 2024-06-02 11:40:33 +01:00
rubenwardy
e1e77033fe Fix deleting soft-removed packages 2024-06-01 15:36:30 +01:00
rubenwardy
1fad818f05 Add review count to scores API 2024-06-01 00:06:25 +01:00
rubenwardy
37bff46f33 Add remove profile picture button 2024-05-26 15:21:57 +01:00
rubenwardy
9cb4d13d71 Add liberapay to FUNDING.yml 2024-05-24 16:31:25 +01:00
rubenwardy
8815327257 Add user_agent is_bot tests 2024-05-24 16:24:38 +01:00
rubenwardy
a3371d538c Fix two form and validation issues 2024-05-24 16:24:38 +01:00
rubenwardy
8191e3fe63 Update remaining Python dependencies 2024-05-24 16:24:38 +01:00
rubenwardy
b5cd169af8 Update database dependencies 2024-05-24 16:24:38 +01:00
rubenwardy
37b50bf409 Update Flask dependencies 2024-05-24 16:24:38 +01:00
John Donne
49a2ee5b82 Translated using Weblate (French)
Currently translated at 94.8% (906 of 955 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2024-05-23 21:09:29 +02:00
Jamil Mohamad Alhussein
14d1621db5 Translated using Weblate (Arabic)
Currently translated at 3.8% (37 of 955 strings)

Added translation using Weblate (Arabic)

Co-authored-by: Jamil Mohamad Alhussein <jamilmohamadalhoseen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ar/
Translation: Minetest/ContentDB
2024-05-23 21:09:28 +02:00
Software In Interlingua
6e6fb20016 Added translation using Weblate (Interlingua)
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
2024-05-23 21:09:27 +02:00
BlackImpostor
3278b1ce22 Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-05-23 21:09:25 +02:00
rubenwardy
04b87a4e74 Add approval stats page 2024-05-17 18:52:55 +01:00
Mićadźoridź
a920854796 Translated using Weblate (Komi)
Currently translated at 0.1% (1 of 955 strings)

Added translation using Weblate (Komi)

Co-authored-by: Mićadźoridź <otto.grotovel@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/kv/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
BlackImpostor
6445f37847 Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
jhon game
6a72def6e9 Translated using Weblate (Hebrew)
Currently translated at 0.1% (1 of 955 strings)

Added translation using Weblate (Hebrew)

Co-authored-by: jhon game <jhongamepc2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/he/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Unacceptium
b9303aa82d Translated using Weblate (Hungarian)
Currently translated at 45.8% (438 of 955 strings)

Co-authored-by: Unacceptium <unacceptium@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Ярослав Рукавицын
21b1f632c2 Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: Ярослав Рукавицын <skybuilderoffical@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Mateusz Malinowski
f8f228112d Translated using Weblate (Polish)
Currently translated at 90.2% (862 of 955 strings)

Co-authored-by: Mateusz Malinowski <bohopicasso@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
孙鑫然
aae43d72a7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: 孙鑫然 <sun_20120302@qq.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
BlackImpostor
b6c2bcb77e Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Gejobo
45ce4cf469 Translated using Weblate (Russian)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: Gejobo <gejobo1652@acname.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Lunovox Heavenfinder
64818f7247 Translated using Weblate (Portuguese)
Currently translated at 63.8% (610 of 955 strings)

Co-authored-by: Lunovox Heavenfinder <lunovox@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Oğuz Ersen
c3fd773523 Translated using Weblate (Turkish)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Wuzzy
2dc5e080d2 Translated using Weblate (German)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
Just Playing
64f7a9a7fc Translated using Weblate (Indonesian)
Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (955 of 955 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: Just Playing <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
gallegonovato
f6d3b4a4b6 Translated using Weblate (Spanish)
Currently translated at 100.0% (955 of 955 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-05-16 18:09:22 +02:00
rubenwardy
b2e543a16a Reduce Sentry sample rate 2024-05-16 17:08:57 +01:00
rubenwardy
aaecfb1121 Use latest version of Postgres 14 2024-05-16 16:56:07 +01:00
rubenwardy
8e719e3503 Fix broken links in reviews hypertext 2024-05-02 20:40:54 +01:00
rubenwardy
4ac0016c0b Add endpoint for getting hypertext of reviews 2024-05-02 20:32:49 +01:00
rubenwardy
faddf11f77 Fix TaskErrors being reported to Sentry 2024-05-01 21:29:36 +01:00
rubenwardy
662c632f5d Fix typos in privacy policy 2024-04-30 23:17:12 +01:00
rubenwardy
3d9fe80177 Add disconnect GitHub button 2024-04-30 23:16:14 +01:00
rubenwardy
a2125acddd Add privacy policy updated note to footer 2024-04-29 23:17:14 +01:00
rubenwardy
4bed2fc40c Add Sentry to about and privacy policy 2024-04-29 23:07:36 +01:00
rubenwardy
31b8ef5d87 Update privacy policy (#445) 2024-04-29 23:04:24 +01:00
rubenwardy
7d18cdee95 Use Sentry instead of emailing errors 2024-04-28 12:35:16 +01:00
rubenwardy
3a794fecbf Add contact and donate links to the footer 2024-04-14 15:51:35 +01:00
rubenwardy
686d285731 Fix hypertext escaping of game links 2024-04-07 23:17:58 +01:00
rubenwardy
f77ecd824c Add username to already linked error message
This doesn't introduce user enumeration as GitHub
username associations were already public
2024-04-07 23:17:34 +01:00
rubenwardy
465370d3fc Add featured field to packages API
Fixes #500
2024-04-05 18:25:41 +01:00
rubenwardy
609354cd35 Hypertext: Fix various issues
* Change link color
* Return absolute URLs
* Provide link to tables (with anchor)
* Provide link to image when include_images=false
* Escape backward slashes
* Make package info more compact
2024-04-05 18:17:07 +01:00
rubenwardy
fc565eee92 Update translations 2024-04-03 18:35:19 +01:00
Oğuz Ersen
64ba3f6e15 Translated using Weblate (Turkish)
Currently translated at 100.0% (964 of 964 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2024-04-03 19:34:45 +02:00
Linerly
756aff4b5b Translated using Weblate (Indonesian)
Currently translated at 100.0% (964 of 964 strings)

Co-authored-by: Linerly <linerly@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-04-03 19:34:45 +02:00
gallegonovato
5fdabdfc9b Translated using Weblate (Spanish)
Currently translated at 100.0% (964 of 964 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-04-03 19:34:45 +02:00
rubenwardy
6280cd5947 Remove some forum topic related features (#527) 2024-04-03 18:30:08 +01:00
rubenwardy
bb81e1387a Update repo/forum link alert message 2024-04-03 18:24:41 +01:00
rubenwardy
1b8c13914c Add admin action to warn about git/repo links instead of internal links 2024-04-03 00:37:21 +01:00
rubenwardy
3ee4b723c1 for-client API: Add links to supported games 2024-04-03 00:27:58 +01:00
rubenwardy
47b2d07e89 for-client API: Make screenshots a list of objects not URLs 2024-04-01 17:43:16 +01:00
rubenwardy
1be4155ab0 Add Minetest-optimised package endpoint 2024-04-01 17:32:12 +01:00
rubenwardy
0f5a97b539 Improve repo and forum topic matching in hints 2024-03-31 15:59:30 +01:00
rubenwardy
792488cce1 Update translations 2024-03-31 15:39:42 +01:00
rubenwardy
66f855cc61 Improve package edit hints 2024-03-31 15:38:46 +01:00
Just Playing
f31bc34d5e Translated using Weblate (Indonesian)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: Just Playing <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
ssantos
1e782140d7 Translated using Weblate (Portuguese)
Currently translated at 58.6% (562 of 958 strings)

Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
复予
360e784c63 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (957 of 958 strings)

Co-authored-by: 复予 <clonewith@qq.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
Wuzzy
ebac0df7df Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
y5nw
15504bae53 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (956 of 958 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-03-31 05:04:28 +02:00
复予
722b0f7dc2 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (956 of 958 strings)

Co-authored-by: 复予 <clonewith@qq.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
Oğuz Ersen
3496d08c13 Translated using Weblate (Turkish)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
Ярослав Рукавицын
b957c8bc58 Translated using Weblate (Russian)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: Ярослав Рукавицын <skybuilderoffical@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
Linerly
8cad92436c Translated using Weblate (Indonesian)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: Linerly <linerly@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
gallegonovato
21687c7558 Translated using Weblate (Spanish)
Currently translated at 100.0% (958 of 958 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2024-03-31 05:04:28 +02:00
rubenwardy
8c59520317 Make package edit help links open in a new tab 2024-03-31 04:04:00 +01:00
rubenwardy
eaea6ce9a3 Add help page for making better package pages 2024-03-31 04:00:18 +01:00
rubenwardy
f0a33927bd Fix collections API showing unapproved packages
Fixes #504
2024-03-30 17:46:54 +00:00
rubenwardy
e82dac4403 Fix collections showing unapproved packages
Fixes #504
2024-03-30 17:41:47 +00:00
rubenwardy
c782e59531 Add link to collections search to package page 2024-03-30 17:37:43 +00:00
rubenwardy
e9193aefb8 Add favorites count to favorite button 2024-03-30 17:27:08 +00:00
rubenwardy
64414a3731 Check that GitHub doesn't pass a null user id 2024-03-30 17:06:32 +00:00
rubenwardy
f5dd77fcb3 Use GitHub user ids instead of usernames for authentication
Otherwise, renaming a GitHub account could allow someone else
to gain access to a CDB account.
2024-03-30 17:00:01 +00:00
rubenwardy
a8d2cc0383 Add dependency-based cache to game support algorithm 2024-03-29 16:02:32 +00:00
rubenwardy
b33a7f79b1 Fix game support not updating when removing a provided mod 2024-03-29 15:54:52 +00:00
rubenwardy
311d07d454 Fix user_supported_games when supporting all games 2024-03-29 15:45:01 +00:00
rubenwardy
43f4d4a7f4 Validate game names given to the support_game field 2024-03-27 22:06:52 +00:00
rubenwardy
b151f78ca6 Fix key error in game support algorithm 2024-03-27 20:00:57 +00:00
rubenwardy
af2bdef1bf Remove game support disclaimer 2024-03-27 19:39:28 +00:00
rubenwardy
434fd03fe8 Fix crash on manual game support override 2024-03-27 19:16:29 +00:00
rubenwardy
2c0d90e797 Rewrite game support algorithm
Fixes #395
2024-03-27 19:03:48 +00:00
rubenwardy
f9048a8f49 Fix rendering of task errors 2024-03-27 17:55:51 +00:00
rubenwardy
6b9614314c Make it clearer that creating a package is only the first step
Fixes #525
2024-03-27 17:47:15 +00:00
rubenwardy
0609176434 Update translations 2024-03-23 16:20:14 +00:00
JUST PLAYING
1f7955b392 Translated using Weblate (Indonesian)
Currently translated at 100.0% (952 of 952 strings)

Co-authored-by: JUST PLAYING <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-23 17:19:14 +01:00
y5nw
4a671e7eef Translated using Weblate (Chinese (Simplified))
Currently translated at 97.4% (928 of 952 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-03-23 17:19:14 +01:00
BlackImpostor
6dd26b00e3 Translated using Weblate (Russian)
Currently translated at 100.0% (952 of 952 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-03-23 17:19:14 +01:00
rubenwardy
ec2acad472 Refactor package approval validation to unify implementation 2024-03-23 16:17:26 +00:00
1F616EMO~nya
f1ec755618 Remove duplicated > (#521) 2024-03-09 15:35:50 +00:00
rubenwardy
0b76982d63 Adjust package score frecency factor 2024-03-07 21:20:32 +00:00
rubenwardy
a79337cc31 Add missing quotes to metric labels 2024-03-07 02:26:14 +00:00
rubenwardy
47feb9edc4 Fix some fields not being cleared in user deactivation 2024-03-06 18:26:01 +00:00
rubenwardy
1d1709d3d4 Add active user and translation prometheus metrics 2024-03-06 18:26:01 +00:00
rubenwardy
824d349c30 Fix typo in use of .rounded-pill
Fixes #520
2024-03-06 18:26:01 +00:00
rubenwardy
a7364990bd Use date as release title in webhook 2024-03-05 23:59:30 +00:00
rubenwardy
a94c398633 Add disable all button to email notification settings 2024-03-05 18:05:13 +00:00
rubenwardy
76638ad878 Require package authors to have an email address 2024-03-05 17:53:47 +00:00
jolesh
a83d3bdbe7 Translated using Weblate (Esperanto)
Currently translated at 18.9% (180 of 952 strings)

Co-authored-by: jolesh <jolesh0815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translation: Minetest/ContentDB
2024-03-04 19:53:44 +01:00
Wuzzy
feb1812f54 Translated using Weblate (German)
Currently translated at 100.0% (952 of 952 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-03-04 19:53:44 +01:00
Oğuz Ersen
070e9c454d Translated using Weblate (Turkish)
Currently translated at 100.0% (952 of 952 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2024-03-04 19:53:44 +01:00
Linerly
166b5fd73a Translated using Weblate (Indonesian)
Currently translated at 100.0% (952 of 952 strings)

Co-authored-by: Linerly <linerly@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-04 19:53:44 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
5e2d0f5680 Translated using Weblate (Malay)
Currently translated at 100.0% (952 of 952 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-03-04 19:53:44 +01:00
rubenwardy
0c98333bcb Translate page: Fix titles being shown in English 2024-03-04 18:23:34 +00:00
rubenwardy
2851c8803c Fix incorrect use of flatpages markdown parser 2024-03-04 18:09:27 +00:00
rubenwardy
2867856d40 Add config for admin contact url 2024-03-04 18:05:16 +00:00
rubenwardy
ba6b7d6dcf Add message to clarify profile picture requirements 2024-03-03 22:50:34 +00:00
rubenwardy
f9c75c2749 Fix .gif profile pictures being imported 2024-03-03 22:44:42 +00:00
rubenwardy
31a47018eb Fix squished user avatars on reviews and threads 2024-03-03 22:44:14 +00:00
rubenwardy
de1332c5e8 Add comment on how to use the translation template.txt 2024-03-03 17:56:58 +00:00
rubenwardy
5983b5c420 Add translation template to package translation page 2024-03-03 17:49:15 +00:00
rubenwardy
3eae7efddd Translate page: Add help link 2024-03-03 15:02:27 +00:00
rubenwardy
3ad97b79dd Add missing rel="ugc" to package page 2024-03-03 15:00:24 +00:00
rubenwardy
5223c2c47b Translate page: Add translation_url to query 2024-03-03 14:51:19 +00:00
rubenwardy
7a108e1199 Update translations 2024-03-03 02:55:11 +00:00
Linerly
f6c761cadf Translated using Weblate (Indonesian)
Currently translated at 100.0% (945 of 945 strings)

Co-authored-by: Linerly <linerly@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-03 03:54:05 +01:00
rubenwardy
dd6f36bd2b Add translators' page 2024-03-03 02:49:27 +00:00
rubenwardy
7c59c1c5b1 Add Vary: Accept-Language to package API 2024-03-03 01:53:12 +00:00
rubenwardy
954a849ba6 Fix crash on deleting failed releases 2024-03-03 01:34:22 +00:00
rubenwardy
1d5be80564 Fix remaining zh_Hans 2024-03-03 01:21:17 +00:00
rubenwardy
f10436b900 Update translations 2024-03-03 01:18:58 +00:00
rubenwardy
8762424c2d Use zh_CN and zh_TW for Chinese 2024-03-03 01:18:24 +00:00
y5nw
61e0904dc9 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (927 of 929 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-03-03 02:16:47 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
e9265a6c91 Translated using Weblate (Malay)
Currently translated at 100.0% (929 of 929 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-03-03 02:16:47 +01:00
Wuzzy
83b7a236fb Translated using Weblate (German)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Oğuz Ersen
955cc8746f Translated using Weblate (Turkish)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Petter Reinholdtsen
9e72ed679a Translated using Weblate (Norwegian Bokmål)
Currently translated at 3.9% (37 of 929 strings)

Co-authored-by: Petter Reinholdtsen <pere-weblate@hungry.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nb_NO/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
BlackImpostor
978c0ca2b5 Translated using Weblate (Russian)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: BlackImpostor <SkyBuilderOFFICAL@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Leo
a1a0a5e79f Translated using Weblate (Ukrainian)
Currently translated at 93.9% (873 of 929 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.1% (856 of 929 strings)

Co-authored-by: Leo <resistancelion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Mumulhl
b4d8022fdf Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (928 of 929 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 95.6% (889 of 929 strings)

Co-authored-by: Mumulhl <mumulhl.666@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
reimu105
54991689b8 Translated using Weblate (Chinese (Traditional))
Currently translated at 19.4% (181 of 929 strings)

Co-authored-by: reimu105 <peter112548@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Mikhail
65e426811b Translated using Weblate (Russian)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: Mikhail <EvPix@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Unacceptium
ce1192260e Translated using Weblate (Hungarian)
Currently translated at 41.0% (381 of 929 strings)

Co-authored-by: Unacceptium <unacceptium@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Giov4
c67214c3ca Translated using Weblate (Italian)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Blood Axe
d0cf94fe51 Translated using Weblate (Norwegian Bokmål)
Currently translated at 3.7% (35 of 929 strings)

Co-authored-by: Blood Axe <bloodaxenor@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nb_NO/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Filippo Alfieri
07714438a2 Translated using Weblate (Italian)
Currently translated at 99.5% (925 of 929 strings)

Co-authored-by: Filippo Alfieri <firealphat0mb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Balázs Kovács
09f8621acc Translated using Weblate (Hungarian)
Currently translated at 40.5% (377 of 929 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
2024-03-03 02:16:47 +01:00
José Douglas
760acbfca2 Translated using Weblate (Portuguese)
Currently translated at 11.0% (103 of 929 strings)

Co-authored-by: José Douglas <josedouglas20002014@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
JUST PLAYING
d37d275f10 Translated using Weblate (Indonesian)
Currently translated at 100.0% (929 of 929 strings)

Co-authored-by: JUST PLAYING <aryadhisuma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
jhh
e4776f9e93 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 51.4% (478 of 929 strings)

Translated using Weblate (Swedish)

Currently translated at 95.6% (889 of 929 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 43.8% (407 of 929 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 43.1% (401 of 929 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 43.0% (400 of 929 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 15.9% (148 of 929 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 5.1% (48 of 929 strings)

Added translation using Weblate (Norwegian Nynorsk)

Co-authored-by: jhh <johshh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nn/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
Unacceptium
c9a1251414 Translated using Weblate (Hungarian)
Currently translated at 40.5% (377 of 929 strings)

Co-authored-by: Unacceptium <unacceptium@proton.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2024-03-03 02:16:47 +01:00
rubenwardy
8f9f554749 Add import translation action 2024-03-03 01:14:33 +00:00
rubenwardy
028452c2ca Add script to clear worker queue 2024-03-03 01:14:33 +00:00
rubenwardy
ffdd0bbafd Add overview stat to languages editor 2024-03-03 01:14:33 +00:00
rubenwardy
fe40a7c6d4 Add package count to languages editor 2024-03-03 01:14:33 +00:00
rubenwardy
b1a9398ed1 Add has_contentdb_translation to Language, add API 2024-03-03 01:14:33 +00:00
rubenwardy
6b34a91241 Fix task/logic errors not being reported in post_release_check_update 2024-03-03 01:14:33 +00:00
rubenwardy
966023be17 Fix textdomain validation in .tr parser 2024-03-03 01:14:33 +00:00
rubenwardy
40d572d645 Add query argument to filter by language support 2024-03-03 01:14:33 +00:00
rubenwardy
3e6d6864b3 Add arguments validation to parse_tr 2024-03-03 01:14:33 +00:00
rubenwardy
e86d9a8e88 Add empty PackageTranslation for all supported languages in a package 2024-03-03 01:14:33 +00:00
rubenwardy
2621e9f7d3 Make QueryBuilder default to get_locale() 2024-03-03 01:14:33 +00:00
rubenwardy
65dc8c0891 Rewrite .tr parser 2024-03-03 01:14:33 +00:00
rubenwardy
1b5791a358 Use package translation on tiles 2024-03-03 01:14:33 +00:00
rubenwardy
9173d3c578 Use package translation in API 2024-03-03 01:14:33 +00:00
rubenwardy
d252d687fc Use translation on package view page 2024-03-03 01:14:33 +00:00
rubenwardy
ab57b6aa2c Add translation url field to package form and API 2024-03-03 01:14:33 +00:00
rubenwardy
9fd182c4fd Add list of languages to package sidebar 2024-03-03 01:14:33 +00:00
rubenwardy
9b36fb2c19 Add package translations page 2024-03-03 01:14:33 +00:00
rubenwardy
658d319eb0 Add translation importing to post_release_check_update 2024-03-03 01:14:33 +00:00
rubenwardy
550a12bdf0 Add .tr file parser 2024-03-03 01:14:33 +00:00
rubenwardy
59e8ca04d9 Add Minetest's supported languages to database 2024-03-03 01:14:33 +00:00
rubenwardy
1656c79c1d Add Languages editor to admin tools 2024-03-03 01:14:33 +00:00
rubenwardy
e138eb9c72 Add Language and PackageTranslation models 2024-03-03 01:14:33 +00:00
rubenwardy
357348c24e Add audit log when mods are added to packages 2024-02-22 12:44:08 +00:00
wsor4035
e25fcd61bc Upgrade ci packages (#515) 2024-02-17 19:40:25 +00:00
rubenwardy
3f2960e7e6 Add game name to search queries filtering by game 2024-01-28 22:35:49 +00:00
rubenwardy
8aa596b31a Link to game hub in supported game lists 2024-01-28 22:35:37 +00:00
rubenwardy
40f23af0bd Game Hub: Add search box for game content
Fixes #512
2024-01-28 22:25:02 +00:00
rubenwardy
142dfefb70 Use consistent filename for JPEG files
Fixes #505
2024-01-28 22:12:29 +00:00
rubenwardy
50b860233b Allow admin tools page to be used by editors
This makes it easier to find certain tools
2024-01-28 21:51:31 +00:00
rubenwardy
4c5b506053 Fix incorrect package list in admin actions 2024-01-27 22:41:42 +00:00
rubenwardy
cbe232ca0c Add expiry to redis ratelimiting 2024-01-22 18:09:26 +00:00
rubenwardy
6bb6a7ae05 Delete release and screenshot uploads immediately 2024-01-18 18:28:36 +00:00
rubenwardy
9ff7567cde Fix 10x bug in admin storage page 2024-01-18 18:14:02 +00:00
rubenwardy
406eb5d180 Add admin page to see package storage usage 2024-01-18 18:09:13 +00:00
rubenwardy
acaf674ec5 Add action to delete empty threads 2024-01-15 00:48:17 +00:00
rubenwardy
77e53b914d Exclude systme messages from rate limit 2024-01-10 00:49:34 +00:00
rubenwardy
8eb3604caf Post to approval thread when package status changes 2024-01-10 00:48:15 +00:00
rubenwardy
8367fd14a8 Prompt users to leave approval thread comment when re-submitting 2024-01-10 00:47:42 +00:00
rubenwardy
2303e70a8e Fix crash on release validation 2024-01-10 00:29:55 +00:00
rubenwardy
5a4238dabc Fix crash when sending emails 2024-01-10 00:16:55 +00:00
rubenwardy
610ed8fca5 Fix releases from "Not Joined" users being unapproved 2024-01-10 00:07:51 +00:00
rubenwardy
69ba1c3fad MinetestCheck: Validate supported_games syntax 2024-01-10 00:07:31 +00:00
rubenwardy
0ffc402d67 Improve "See more" button placement
Fixes #297 and #508
2024-01-05 18:31:10 +00:00
rubenwardy
bfe48924c7 Improve cookie parameters 2024-01-04 23:10:08 +00:00
rubenwardy
7ce2ee1f5b Remove game jam banner 2024-01-04 23:09:03 +00:00
rubenwardy
376864db1b Add caching to more API endpoints 2024-01-01 17:35:11 +00:00
rubenwardy
9e97a06f70 Update non-free help page 2023-12-30 18:11:37 +00:00
rubenwardy
785c931890 Add filter to hide nonfree content
Fixes #121
2023-12-30 18:07:00 +00:00
rubenwardy
ca3436be0c Improve license check message 2023-12-30 17:30:49 +00:00
rubenwardy
c565f0bb50 Add check for license files to release validation 2023-12-30 17:20:55 +00:00
rubenwardy
35701b1097 Add query arg to show packages with flag 2023-12-30 17:09:10 +00:00
rubenwardy
a9ae14af9a Update translations 2023-12-30 16:47:00 +00:00
Alexsandro Vítor
5213579a6b Translated using Weblate (Portuguese)
Currently translated at 6.8% (61 of 891 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 62.7% (559 of 891 strings)

Co-authored-by: Alexsandro Vítor <alexsandro.vitor97@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2023-12-30 17:45:59 +01:00
Giov4
9d1888a651 Translated using Weblate (Italian)
Currently translated at 94.6% (843 of 891 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-12-30 17:45:59 +01:00
José Muñoz
11dc8514ab Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-30 17:45:59 +01:00
Jun Nogata
e887f93427 Translated using Weblate (Japanese)
Currently translated at 24.3% (217 of 891 strings)

Translated using Weblate (Japanese)

Currently translated at 20.0% (179 of 891 strings)

Co-authored-by: Jun Nogata <nogajun@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
Translation: Minetest/ContentDB
2023-12-30 17:45:59 +01:00
reimu105
fc13f70813 Translated using Weblate (Chinese (Traditional))
Currently translated at 19.9% (178 of 891 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 18.5% (165 of 891 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 17.3% (155 of 891 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 17.2% (154 of 891 strings)

Co-authored-by: reimu105 <peter112548@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2023-12-30 17:45:59 +01:00
rubenwardy
41477980df Allow auto-approval for all users that aren't banned 2023-12-29 11:36:25 +00:00
rubenwardy
0488b129fc Add user storage usage to modtools page 2023-12-29 10:38:27 +00:00
rubenwardy
531d6acce5 Add release size to releases page when editing 2023-12-29 10:28:53 +00:00
rubenwardy
5f658f7a1e Add storage usage to releases page 2023-12-29 10:28:00 +00:00
rubenwardy
e5f5313156 Add warning when max_minetest_version is set to the latest stable 2023-12-26 14:50:55 +00:00
rubenwardy
15bde2461e Game Jam: Update message 2023-12-22 02:13:18 +00:00
rubenwardy
44cf1623c5 OAuth2: Treat empty parameter as no parameter 2023-12-16 22:05:52 +00:00
rubenwardy
d69331796b Readd Lato font
Thanks @rollerozxa for showing me an easy way to self-host Google Fonts
2023-12-16 17:24:43 +00:00
rubenwardy
e8a879b7ce Bump bootstrap version to cache invalidate 2023-12-16 16:53:07 +00:00
rubenwardy
70869d4404 Remove use of Google fonts
Fixes #399
2023-12-16 16:50:35 +00:00
rubenwardy
2bd556c00d Add max-age to scss endpoint 2023-12-16 16:22:12 +00:00
rubenwardy
28864740a0 Remove no-cache from thumbnail cache control 2023-12-16 16:16:41 +00:00
rubenwardy
9e6699c549 Collection Editor: Use id rather than vague class 2023-12-16 01:03:27 +00:00
rubenwardy
f946e8db21 Remove donation ad 2023-12-16 01:01:45 +00:00
rubenwardy
4358882105 Fix easymde assets being included on every page 2023-12-16 00:57:13 +00:00
rubenwardy
8606f596f3 Package API: Add option to return desc as hypertext 2023-12-16 00:46:11 +00:00
rubenwardy
e6bba7d8a2 Use bash not sh for util scripts 2023-12-15 23:49:54 +00:00
rubenwardy
4ef3aae193 Make util scripts work on both docker-compose v1 and v2 2023-12-15 23:46:07 +00:00
rubenwardy
8e312c4bcc OAuth2: Return success=true in POST request 2023-12-15 23:03:58 +00:00
rubenwardy
e9911e85a2 Add release download size to API and web interface 2023-12-15 16:33:34 +00:00
Warr1024
0e5158704e Allow WebP screenshots (#503) 2023-12-15 16:23:07 +00:00
rubenwardy
c6a59701be Fix size limit not being applied to Git releases
Fixes #495
2023-12-15 16:19:06 +00:00
rubenwardy
a29345bd10 Clean up various things 2023-12-15 16:08:12 +00:00
rubenwardy
c7b215fcca setup.py: Remove unused and broken -d arg 2023-12-15 16:08:12 +00:00
rubenwardy
cc6f561cfe Update docs: uploads owner and deleting datbases 2023-12-15 16:08:12 +00:00
rubenwardy
36c63b4657 Fix crash on review voting on homepage 2023-12-04 10:24:18 +00:00
rubenwardy
a1a03d6de4 Fix "and" not being translatable 2023-12-03 13:48:57 +00:00
rubenwardy
b80ce88bc0 Add warning when removing a package will break mods 2023-12-03 13:47:52 +00:00
Nisa Syazwani
54a4eb2ac8 Translated using Weblate (Malay)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Nisa Syazwani <nisasyazwani@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Matyáš Pilz
2b3f036f31 Translated using Weblate (Czech)
Currently translated at 61.7% (550 of 891 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Gao Tiesuan
91ab321a53 Translated using Weblate (Chinese (Simplified))
Currently translated at 93.6% (834 of 891 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 84.3% (752 of 891 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Mxt Appz
c8c0500047 Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Mxt Appz <mxtappz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
nyommer
9b1ea7cf92 Translated using Weblate (Hungarian)
Currently translated at 39.1% (349 of 891 strings)

Co-authored-by: nyommer <jishnu.ifeoluwa@fullangle.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
gallegonovato
3cee1e72f9 Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Spurnita
ad15e1016b Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Spurnita <joaquim.puig@upc.edu>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
gallegonovato
9847af13a0 Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Ritwik
938c548421 Added translation using Weblate (Hindi)
Co-authored-by: Ritwik <ritwikraghav14@gmail.com>
2023-12-03 14:10:05 +01:00
Giov4
b1919669ce Translated using Weblate (Italian)
Currently translated at 94.6% (843 of 891 strings)

Translated using Weblate (Italian)

Currently translated at 94.5% (842 of 891 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
John Donne
e551f6219c Translated using Weblate (French)
Currently translated at 98.3% (876 of 891 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Wuzzy
6cfece797d Translated using Weblate (German)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Spurnita
4fe405a125 Translated using Weblate (Spanish)
Currently translated at 100.0% (891 of 891 strings)

Translated using Weblate (Catalan)

Currently translated at 2.6% (24 of 891 strings)

Added translation using Weblate (Catalan)

Co-authored-by: Spurnita <joaquim.puig@upc.edu>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ca/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Muhammad Rifqi Priyo Susanto
b911c9c758 Translated using Weblate (Indonesian)
Currently translated at 100.0% (891 of 891 strings)

Translated using Weblate (Javanese)

Currently translated at 13.2% (118 of 891 strings)

Translated using Weblate (Javanese)

Currently translated at 13.2% (118 of 891 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/jv/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
ROllerozxa
aa28f7415a Translated using Weblate (Swedish)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Translator
615549b433 Translated using Weblate (French)
Currently translated at 98.2% (875 of 891 strings)

Co-authored-by: Translator <kvb@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Christian Elbrianno
9ec6a57919 Added translation using Weblate (Javanese)
Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
2023-12-03 14:10:05 +01:00
Ярослав Рукавицын
95f5599c9c Translated using Weblate (Russian)
Currently translated at 100.0% (891 of 891 strings)

Co-authored-by: Ярослав Рукавицын <skybuilderoffical@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
3raven
deb2550db3 Translated using Weblate (French)
Currently translated at 98.0% (874 of 891 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Lemente
eaaf3d7b5a Translated using Weblate (French)
Currently translated at 98.0% (874 of 891 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-12-03 14:10:05 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
20dd384636 Translated using Weblate (Malay)
Currently translated at 100.0% (891 of 891 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
2023-12-03 14:10:05 +01:00
rubenwardy
884e73e046 Add gamejam 2023 banner 2023-12-02 13:11:57 +00:00
rubenwardy
12664a4f41 Fix crash on review votes page 2023-11-19 13:49:36 +00:00
rubenwardy
2e8ddb8ca4 Improve documentation on repo field 2023-11-19 13:39:38 +00:00
rubenwardy
8619433b66 Homepage: Improve performance by adjusting loading options 2023-11-18 11:25:22 +00:00
rubenwardy
96c86cf070 Homepage: Fix unapproved packages appearing in recently updated 2023-11-18 11:03:49 +00:00
rubenwardy
588945d2dc Readd EasyMDE scripts 2023-11-17 23:58:59 +00:00
rubenwardy
b36e91044f Homepage: Optimise recently updated query 2023-11-17 22:54:38 +00:00
rubenwardy
9184f1bcc0 Add support CDB to footer 2023-11-17 19:28:05 +00:00
rubenwardy
d2feddea1e Allow using the details tag in markdown
Fixes #487
2023-11-14 01:14:50 +00:00
rubenwardy
739179a152 Fix crash on user deletion due to game support 2023-11-13 17:44:40 +00:00
rubenwardy
fa59113cd3 Set max-age in thumbnails endpoint 2023-11-12 16:18:20 +00:00
rubenwardy
b4c508ebab Use lossless webp, fix jpg generation 2023-11-12 16:11:38 +00:00
rubenwardy
c546eef6a9 Add jpg to allowed thumbnail extensions 2023-11-10 19:13:48 +00:00
rubenwardy
4578cb157f Use webp for thumbnails 2023-11-10 19:10:52 +00:00
rubenwardy
5ce5684ca6 Collections: Add short description to profile pages 2023-11-07 23:31:15 +00:00
rubenwardy
bd46943c63 Collections: Add ability to pin to profile 2023-11-07 23:25:53 +00:00
rubenwardy
9b0f84bac5 OAuth: Add app type (is_clientside) 2023-11-07 23:06:22 +00:00
rubenwardy
f74931633c OAuth: Add description 2023-11-07 22:57:19 +00:00
rubenwardy
d4b1344f6a Fix review voting JS not showing removals
Fixes #474
2023-10-31 23:28:20 +00:00
rubenwardy
3279e00aa4 OAuth2: Improve authorize page formatting 2023-10-31 21:18:28 +00:00
Buckaroo Banzai
c09f190712 Add missing \ in oauth curl example (#486)
Co-authored-by: BuckarooBanzay <BuckarooBanzay@users.noreply.github.com>
2023-10-31 20:57:23 +00:00
rubenwardy
047bf936b4 OAuth2: Allow normal users to create clients (but unapproved) 2023-10-31 20:29:49 +00:00
rubenwardy
fa389273ab OAuth2: Fix typo 2023-10-31 20:14:21 +00:00
rubenwardy
00f7dbb28d OAuth2: Add approval and verified apps 2023-10-31 20:11:55 +00:00
rubenwardy
073dcf9517 OAuth2: Improve authorize page wording 2023-10-31 19:50:29 +00:00
rubenwardy
8b03ca6c63 OAuth2: Add ability to revoke all tokens 2023-10-31 19:38:32 +00:00
rubenwardy
e0553d0a50 OAuth2: Add example authorize URL to edit form 2023-10-31 19:26:23 +00:00
rubenwardy
76f9f58175 OAuth2: Add audit logs 2023-10-31 19:18:08 +00:00
rubenwardy
540603ed7a OAuth2 docs: check access token + scopes 2023-10-31 18:58:37 +00:00
rubenwardy
bc38094a41 Remove dummy data from API 2023-10-31 18:54:58 +00:00
rubenwardy
c4fac34e6a Fix skip button not being styled 2023-10-31 18:50:33 +00:00
rubenwardy
5ab6b84fe7 Add delete-token API 2023-10-31 18:50:33 +00:00
rubenwardy
604fb010d2 Fix codehilite guessing languages 2023-10-31 18:50:33 +00:00
rubenwardy
72b608b158 Respect next when logging in using GitHub 2023-10-31 18:50:33 +00:00
rubenwardy
a29715775e Add OAuth2 Applications API
Fixes #344
2023-10-31 18:45:45 +00:00
rubenwardy
1627fa50f2 Enable Spanish translation 2023-10-23 22:36:48 +01:00
rubenwardy
3855ca1361 Fix error in Brazil translation 2023-10-23 22:20:24 +01:00
rubenwardy
9e39f5e155 Update translations 2023-10-23 22:17:05 +01:00
Bas Huis
d37ff4a55c Translated using Weblate (Dutch)
Currently translated at 60.2% (534 of 886 strings)

Co-authored-by: Bas Huis <bassimhuis@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nl/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
Ярослав Рукавицын
1918e93421 Translated using Weblate (Russian)
Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: Ярослав Рукавицын <skybuilderoffical@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
José Muñoz
1705130d64 Translated using Weblate (Swedish)
Currently translated at 95.4% (846 of 886 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
gallegonovato
38ea454585 Translated using Weblate (Spanish)
Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
Lemente
f74ab6ed77 Translated using Weblate (French)
Currently translated at 97.1% (861 of 886 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
José Muñoz
70b2d4fbcd Translated using Weblate (Spanish)
Currently translated at 83.4% (739 of 886 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
Wuzzy
97aba174a6 Translated using Weblate (German)
Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
Matyáš Pilz
c7ef3e6810 Translated using Weblate (Czech)
Currently translated at 62.0% (550 of 886 strings)

Translated using Weblate (Czech)

Currently translated at 61.3% (544 of 886 strings)

Co-authored-by: Matyáš Pilz <matys.pilz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2023-10-23 23:07:29 +02:00
Farooq Karimi Zadeh
179326973e Translated using Weblate (Persian)
Currently translated at 12.3% (109 of 886 strings)

Co-authored-by: Farooq Karimi Zadeh <fkz@riseup.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fa/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
Tirifto
2fbec35746 Translated using Weblate (Esperanto)
Currently translated at 18.7% (166 of 886 strings)

Co-authored-by: Tirifto <tirifto@posteo.cz>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
José Douglas
0bfb7c0509 Translated using Weblate (Portuguese (Brazil))
Currently translated at 57.7% (512 of 886 strings)

Added translation using Weblate (Portuguese)

Co-authored-by: José Douglas <josedouglas20002014@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
nyommer
a78e4e171e Translated using Weblate (Hungarian)
Currently translated at 39.0% (346 of 886 strings)

Translated using Weblate (Hungarian)

Currently translated at 38.9% (345 of 886 strings)

Co-authored-by: nyommer <jishnu.ifeoluwa@fullangle.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
Jakub Z
8998fe9241 Translated using Weblate (Polish)
Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
Giov4
898cd547b6 Translated using Weblate (Italian)
Currently translated at 94.4% (837 of 886 strings)

Translated using Weblate (Italian)

Currently translated at 91.1% (808 of 886 strings)

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

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
Linerly
4aeb694131 Translated using Weblate (Indonesian)
Currently translated at 100.0% (886 of 886 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-10-23 23:07:28 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
1bdc3bbb42 Translated using Weblate (Malay)
Currently translated at 100.0% (886 of 886 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (886 of 886 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
2023-10-23 23:07:28 +02:00
rubenwardy
e402b52221 Improve repo wizard messaging on create package 2023-10-23 21:50:57 +01:00
rubenwardy
a050be734c Update some dependencies 2023-10-20 22:49:42 +01:00
rubenwardy
a9533732f3 Rename "unapprove" to "unpublish" 2023-10-15 22:16:19 +01:00
rec
40a59a4d31 Adding in a carousel for packages (#475)
Co-authored-by: recluse4615 <recluse4615@users.noreply.github.com>
2023-10-15 22:14:08 +01:00
rubenwardy
fc559814d4 Prevent renaming published packages 2023-10-15 22:13:35 +01:00
rubenwardy
4fc54f12bc Remove optional banner from set password page 2023-10-11 22:33:37 +01:00
rubenwardy
0b7febae5d Stats: Point to spike blog post 2023-10-08 14:56:12 +01:00
rubenwardy
9db2fdd49a Add missing unique constraint for author and name
Swear this was already present
2023-10-02 00:32:20 +01:00
rubenwardy
16f765d0af Fix crash due to missing app context in setup.py 2023-10-02 00:24:05 +01:00
rubenwardy
7c72912913 Transfer: add option to remove current owner from maintainers 2023-10-02 00:23:30 +01:00
rubenwardy
4f4e5f8e53 Prevent transferring if destination package exists 2023-10-02 00:19:55 +01:00
rubenwardy
0ecf992f83 Post package removals to Discord webhook 2023-10-01 23:41:30 +01:00
rubenwardy
43af3a8e75 Improve user comments page performance 2023-09-29 23:10:08 +01:00
rubenwardy
315337d552 Allow indexing collections list page 2023-09-15 22:06:18 +01:00
rubenwardy
bcebb72a66 Include aliases in /api/updates/ 2023-09-14 20:41:48 +01:00
rubenwardy
83e7701eee Add a super fast latest release API 2023-09-12 21:15:55 +01:00
rubenwardy
886dec3ffd Fix crash on unauthenticated user 2023-09-02 22:45:50 +01:00
rubenwardy
383f9a43ef Move font awesome above scripts to avoid visual jump 2023-09-02 22:37:52 +01:00
rubenwardy
8dfd5c407d Readd "Sync with Forums" button to profile picture settings 2023-09-02 22:34:29 +01:00
rubenwardy
459eb02112 Make user comments page paginated
Fixes #392
2023-09-02 22:27:38 +01:00
rubenwardy
30722020c8 Flatpages: Move table of contents to the top on mobile
Fixes #459
2023-09-02 22:05:11 +01:00
rubenwardy
d4ecaee5f2 Fix crash when using limit in /api/packages/
Fixes #468
2023-09-02 21:52:14 +01:00
rubenwardy
b6995b1857 Fix UI tests due to increased password requirements 2023-09-02 21:49:13 +01:00
rubenwardy
af3c4fe987 Fix crash when saving empty collection
Fixes #464
2023-09-02 21:46:25 +01:00
rubenwardy
e94ff23bb9 Fix crash when bulk updating package configs
Fixes #467
2023-09-02 21:38:52 +01:00
rubenwardy
566d557840 Prevent editing game support until a release is created 2023-09-02 21:37:20 +01:00
rubenwardy
aa87bee014 Pin Python to 3.10.11
Python 3.10.12 and above causes threading issues (ex: "getaddrinfo() thread failed to start")
2023-09-02 21:33:16 +01:00
rubenwardy
379337ad60 Fix crash when collection.author is None 2023-08-31 09:42:37 +01:00
rubenwardy
18e8a11d00 Add improved "How to install" help page 2023-08-28 09:28:38 +01:00
rubenwardy
9ec2b05e8d Create favorites collection when viewing /add-to/ 2023-08-28 08:54:59 +01:00
rubenwardy
bef3c2f8f0 Update translations 2023-08-27 13:14:53 +01:00
Giov4
69b584d8b3 Translated using Weblate (Italian)
Currently translated at 91.3% (806 of 882 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-08-27 14:13:55 +02:00
Muhammad Rifqi Priyo Susanto
d5d3e70a48 Translated using Weblate (Indonesian)
Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-08-27 14:13:55 +02:00
Emmily
90b6b970ec Translated using Weblate (Esperanto)
Currently translated at 18.3% (162 of 882 strings)

Translated using Weblate (Spanish)

Currently translated at 84.0% (741 of 882 strings)

Co-authored-by: Emmily <Emmilyrose779@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-08-27 14:13:55 +02:00
Linerly
dbcbc6bedb Translated using Weblate (Indonesian)
Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-08-27 14:13:55 +02:00
rubenwardy
4c0e181336 Use same hue for link color as primary color 2023-08-26 19:55:14 +01:00
rubenwardy
3f2c7094d9 Add missing .btn to report button 2023-08-26 19:50:53 +01:00
rubenwardy
df825a0a84 Make links blue and not underlined 2023-08-26 19:49:23 +01:00
rubenwardy
4d470ce230 Increase min password length to 12 2023-08-26 14:54:58 +01:00
rubenwardy
da17fb63f3 Add review, comment, and collection counts to prometheus metrics 2023-08-26 13:52:29 +01:00
rubenwardy
416674e7ee Allow submitting reviews with ctrl+enter 2023-08-26 13:41:32 +01:00
rubenwardy
8f52c67f0f Allow submitting comment with ctrl+enter
Fixes #427
2023-08-26 13:39:03 +01:00
rubenwardy
798679ca44 Use async in polltask.js 2023-08-26 13:21:20 +01:00
rubenwardy
c8a30a27dc Move JS files to /static/js/ 2023-08-26 13:08:11 +01:00
rubenwardy
2f458ba40e Remove direct use of jQuery
jQuery is still required by jQuery UI
2023-08-26 13:03:29 +01:00
rubenwardy
9eb03c6a57 Set homepage row size to 4 2023-08-25 21:31:19 +01:00
rubenwardy
98c1cbc769 Remove lazy loading from carousel 2023-08-25 21:28:39 +01:00
rubenwardy
05a597adeb Fix package tile images not fully covering tile 2023-08-25 20:55:50 +01:00
rubenwardy
0649e5cf13 Lazy load images 2023-08-25 20:50:48 +01:00
rubenwardy
210a0a10ae Update jQuery dependencies 2023-08-25 20:49:17 +01:00
rubenwardy
e99dbda126 Fix underline in UI-autocomplete 2023-08-22 20:12:56 +01:00
rubenwardy
9df80d212e Upgrade to Bootstrap v5 (#457) 2023-08-22 19:58:43 +01:00
rubenwardy
70362ff7a6 Update two dependencies 2023-08-22 00:52:18 +01:00
rubenwardy
5f1d0ed946 Disable quick_review_voting.js for guest users 2023-08-22 00:48:08 +01:00
rubenwardy
4df15d6ff2 Fix "featured" being used instead of "spotlight" on homepage 2023-08-22 00:16:32 +01:00
rubenwardy
954826f053 Fix new items missing drag handles in collection editor 2023-08-21 00:34:55 +01:00
rubenwardy
dca6e82594 Add JS to vote on reviews without form submit
Fixes #329
2023-08-21 00:13:22 +01:00
rubenwardy
2a9f2924da Improve documentation on package create page
Fixes #447
2023-08-20 23:41:23 +01:00
rubenwardy
4433918d4c Add required text next to required fields 2023-08-20 23:41:06 +01:00
rubenwardy
bb719ad844 Show JSON decode errors to users
Fixes #431
2023-08-20 23:17:54 +01:00
rubenwardy
1b5174621d Add warning about favorites being public 2023-08-20 23:04:09 +01:00
Dominik Gęgotek
ef18f255be Translated using Weblate (Polish)
Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Dominik Gęgotek <ioutora@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2023-08-20 23:57:29 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
aac583e33b Translated using Weblate (Malay)
Currently translated at 100.0% (882 of 882 strings)

Translated using Weblate (Malay)

Currently translated at 96.3% (850 of 882 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
2023-08-20 23:57:29 +02:00
rubenwardy
e5e68826fb Allow ordering packages in collections 2023-08-20 22:51:21 +01:00
rubenwardy
4bd53e4b1a Use collections for spotlight and featured, remove protected tags 2023-08-20 22:25:54 +01:00
rubenwardy
a2ea6573bd Add Collections API 2023-08-20 21:47:20 +01:00
rubenwardy
c0655eb9e2 Fix package grid tiles being too large
Fixes #456
2023-08-19 03:03:15 +01:00
rubenwardy
b410ab3bcc Improve behaviour of added packages in search box 2023-08-19 02:56:05 +01:00
rubenwardy
618a768f9a Fix not being able to remove packages added by query field 2023-08-19 02:48:40 +01:00
rubenwardy
cea315048b Add long description to collections 2023-08-19 02:43:38 +01:00
rubenwardy
c04cb14eec Fix placeholder not being shown in collection view 2023-08-19 02:33:16 +01:00
rubenwardy
5b4f997f3d Add ability to add packages from collection page 2023-08-19 02:31:40 +01:00
rubenwardy
57ba3e8700 Add ability to remove packages from collection page 2023-08-19 01:25:13 +01:00
rubenwardy
8d97c6b38e Update translations 2023-08-19 00:52:02 +01:00
Wuzzy
c1c272376f Translated using Weblate (German)
Currently translated at 100.0% (852 of 852 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
Muhammad Rifqi Priyo Susanto
a28644548f Translated using Weblate (Indonesian)
Currently translated at 100.0% (852 of 852 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
José Muñoz
9a08b53bf6 Translated using Weblate (Spanish)
Currently translated at 86.9% (741 of 852 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
Jakub Z
bba59bf96a Translated using Weblate (Polish)
Currently translated at 100.0% (852 of 852 strings)

Translated using Weblate (Polish)

Currently translated at 96.4% (822 of 852 strings)

Translated using Weblate (Polish)

Currently translated at 95.4% (813 of 852 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
古銭マニア
caa104bbfb Translated using Weblate (Japanese)
Currently translated at 21.1% (180 of 852 strings)

Co-authored-by: 古銭マニア <kosennotaku128@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
ROllerozxa
1b74c4c520 Translated using Weblate (Swedish)
Currently translated at 100.0% (852 of 852 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
John Donne
0dcb084a4f Translated using Weblate (French)
Currently translated at 96.9% (826 of 852 strings)

Translated using Weblate (French)

Currently translated at 96.0% (818 of 852 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
AlexTECPlayz
8b79340180 Translated using Weblate (Romanian)
Currently translated at 8.2% (70 of 852 strings)

Co-authored-by: AlexTECPlayz <alextec70@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ro/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
Linerly
340edaa78a Translated using Weblate (Indonesian)
Currently translated at 100.0% (852 of 852 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-08-19 01:49:55 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
2b057a2d50 Translated using Weblate (Malay)
Currently translated at 100.0% (852 of 852 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
2023-08-19 01:49:55 +02:00
rubenwardy
e47ea249e7 Use Star instead of Heart for favourites 2023-08-19 00:46:27 +01:00
rubenwardy
c1aa12dc8c Make "Report an Issue" translatable 2023-08-19 00:43:42 +01:00
rubenwardy
b3847da28e Add report issue button to review form 2023-08-19 00:37:50 +01:00
rubenwardy
2c52f06744 Support lists of packages in create collection 2023-08-19 00:33:04 +01:00
rubenwardy
aaee730ba5 Fix crash when creating collection 2023-08-16 09:43:26 +01:00
rubenwardy
eb81674f06 Prevent naming a package the same as a collection 2023-08-16 01:03:54 +01:00
rubenwardy
ea2f1f4f6f Fix unapproved packages appearing in collections 2023-08-16 01:00:51 +01:00
rubenwardy
f470357a42 Allow changing collection URL name 2023-08-16 00:54:34 +01:00
rubenwardy
2ad25f1aa9 Fix various issues with collections 2023-08-15 20:39:32 +01:00
rubenwardy
af4f03d298 Add ability to delete collections 2023-08-14 22:17:29 +01:00
rubenwardy
4a0653bcfd Fix responsiveness of package grid on mobile 2023-08-14 22:01:01 +01:00
rubenwardy
f7a5a1218f Add package collections
Fixes #378
2023-08-14 21:48:50 +01:00
rubenwardy
bf20177756 Use CSS grid for package tiles 2023-08-14 18:55:42 +01:00
rubenwardy
800cacb003 Add API for HTML/Markdown to Hypertext 2023-08-13 13:50:02 +01:00
rubenwardy
2454738eaa Fix banned users having incorrect rank after account deactivation 2023-08-13 13:29:55 +01:00
rubenwardy
f1b2465e82 Add sorting links to profile page 2023-08-13 13:29:32 +01:00
rubenwardy
32a305c9d8 Prevent deleting banned accounts 2023-08-10 16:31:16 +01:00
rubenwardy
a1eac9959e Reduce min comment length 2023-08-06 20:55:55 +01:00
rubenwardy
7492c308ad MinetestCheck: Add more reserved mod names 2023-08-06 12:29:43 +01:00
rubenwardy
d31162a1fa MinetestCheck: Forbid the use of reserved mod names 2023-08-06 12:21:06 +01:00
rubenwardy
92a9a7268c Use restart: unless-stopped instead of restart: always 2023-07-29 21:34:37 +01:00
rubenwardy
76414cb5ba Add page to transfer packages 2023-07-29 21:34:23 +01:00
Dmitry Smirnov
6d184e0320 Allow users to see their own comments (#451)
Fixes #428
2023-07-22 13:13:12 +01:00
rubenwardy
30372b99c6 Disallow packages that ask mod security to be disabled 2023-07-22 12:48:20 +01:00
rubenwardy
18ee0108e5 Use display name rather than username in Discord webhooks 2023-07-22 12:45:50 +01:00
rubenwardy
d374ce27cf Make package approval threads private
We've found that a lot of users comment in these
threads when they shouldn't
2023-07-22 12:45:21 +01:00
rubenwardy
c24013435c Add restart: always to docker-compose.yml 2023-07-22 08:49:54 +01:00
rubenwardy
2007f3a095 Add og:image to profile pages 2023-07-15 19:11:41 +01:00
rubenwardy
a0c0cce2ab Fix og:title not being set unless description is set 2023-07-15 19:09:47 +01:00
rubenwardy
4a2d5c9066 Redirect /packages/author/ to /users/author/ 2023-07-15 19:08:36 +01:00
rubenwardy
9e446e7524 Add thread id and URL to review API 2023-07-12 21:32:22 +01:00
rubenwardy
43e9641feb API: Add option to filter reviews by package author 2023-07-09 16:48:46 +01:00
rubenwardy
e446e9011a Adjust error message to debug Yandex mail issue 2023-07-06 17:04:59 +01:00
rubenwardy
2494121615 Fix inverted condition in supports_all_games check 2023-07-03 22:49:46 +01:00
rubenwardy
a3e8dce871 Update game support docs 2023-07-03 22:36:25 +01:00
rubenwardy
ba0b4d518d Prevent supporting all games from .conf when depending on a game-specific game 2023-07-03 22:23:33 +01:00
rubenwardy
1f2478fc1b Add list of packages affected by bulk support games 2023-06-27 22:55:46 +01:00
rubenwardy
1a8d28a2d8 Prevent users being able to enable support for all games when they shouldn't 2023-06-27 21:40:31 +01:00
rubenwardy
96f9adb95f Update translations 2023-06-27 21:02:03 +01:00
ROllerozxa
15162e7860 Translated using Weblate (Swedish)
Currently translated at 96.4% (821 of 851 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-06-27 22:00:11 +02:00
José Muñoz
ec0a9296d8 Translated using Weblate (Spanish)
Currently translated at 86.3% (735 of 851 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-06-27 22:00:11 +02:00
rubenwardy
86565a0384 Fix unsupported games appearing in game hubs
Fixes #449
2023-06-27 20:41:07 +01:00
Wuzzy
870efc7fbe Translated using Weblate (German)
Currently translated at 100.0% (851 of 851 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-06-26 23:40:24 +02:00
Giov4
998db5d26d Translated using Weblate (Italian)
Currently translated at 94.8% (807 of 851 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-06-26 23:40:23 +02:00
Robinson
8593747712 Translated using Weblate (Czech)
Currently translated at 60.7% (517 of 851 strings)

Co-authored-by: Robinson <simekm@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2023-06-26 23:40:23 +02:00
Linerly
19969abf63 Translated using Weblate (Indonesian)
Currently translated at 100.0% (851 of 851 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-06-26 23:40:22 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
6d7cfd1ca1 Translated using Weblate (Malay)
Currently translated at 100.0% (851 of 851 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
2023-06-26 23:40:22 +02:00
rubenwardy
78f0d1f6c3 Fix crash due to incorrect /tasks/ param type 2023-06-25 12:20:23 +01:00
rubenwardy
adbc2b0195 Fix missing types on integer route arguments 2023-06-25 00:29:24 +01:00
rubenwardy
461d45b411 Fix typos in game support text 2023-06-23 12:11:21 +01:00
rubenwardy
0d4164c5b1 Update translations 2023-06-20 00:38:56 +01:00
Linerly
43707a5416 Translated using Weblate (Indonesian)
Currently translated at 100.0% (849 of 849 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-06-20 01:38:27 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
c98ab982a2 Translated using Weblate (Malay)
Currently translated at 100.0% (849 of 849 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
2023-06-20 01:38:27 +02:00
rubenwardy
dcce351ad2 Improve messages relating to game support 2023-06-20 00:36:33 +01:00
rubenwardy
edce45f71a Add emails_sent metric 2023-06-19 23:34:49 +01:00
rubenwardy
3992b30cc2 Allow standard users to override game support 2023-06-19 22:35:26 +01:00
rubenwardy
c7ee42a4d5 Game Support: Show correct conf file name 2023-06-19 22:33:45 +01:00
rubenwardy
bb41ea7dcc Prevent texture packs from supporting all games 2023-06-19 22:22:55 +01:00
rubenwardy
f2eee008f6 Fix endpoint argument rename causing issues 2023-06-19 22:10:21 +01:00
rubenwardy
45ed12ddf0 Use snake_case for method names 2023-06-19 21:57:54 +01:00
rubenwardy
16f93b3e13 Optimise imports and fix linter issues 2023-06-19 21:57:54 +01:00
rubenwardy
0ddf498285 Fix tests 2023-06-19 21:57:54 +01:00
rubenwardy
d808a5c822 Fix issues based on linter 2023-06-19 21:57:54 +01:00
rubenwardy
f2cfb6c17d Fix typos and grammar issues 2023-06-19 21:57:54 +01:00
rubenwardy
e8b14709e6 Fix quotes in templates 2023-06-19 21:57:54 +01:00
rubenwardy
8585357942 Use consistent naming scheme for methods/functions 2023-06-19 21:57:54 +01:00
rubenwardy
e0b25054dc Fix incorrect filter on bulk game support set 2023-06-18 23:03:41 +01:00
rubenwardy
f3d21b79ab Improve game support messages 2023-06-18 22:57:24 +01:00
rubenwardy
324d7ec1e1 Add audit logging for game support pages 2023-06-18 21:34:53 +01:00
rubenwardy
e6f36113ce Update translations 2023-06-18 21:21:04 +01:00
Giov4
eabacbf421 Translated using Weblate (Italian)
Currently translated at 99.0% (817 of 825 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
Robinson
375285162b Translated using Weblate (Czech)
Currently translated at 49.0% (405 of 825 strings)

Translated using Weblate (Czech)

Currently translated at 39.3% (325 of 825 strings)

Co-authored-by: Martin Šimek <simekm@yahoo.com>
Co-authored-by: Robinson <simekm@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
Nicolae Crefelean
cdb1bf0963 Translated using Weblate (Romanian)
Currently translated at 8.3% (69 of 825 strings)

Translated using Weblate (Romanian)

Currently translated at 7.5% (62 of 825 strings)

Translated using Weblate (Romanian)

Currently translated at 1.2% (10 of 825 strings)

Added translation using Weblate (Romanian)

Co-authored-by: Nicolae Crefelean <kneekoo@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ro/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
Linerly
68ccf63486 Translated using Weblate (Indonesian)
Currently translated at 100.0% (825 of 825 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
José Muñoz
a9094ea53d Translated using Weblate (Spanish)
Currently translated at 89.4% (738 of 825 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
Translator
5736384377 Translated using Weblate (French)
Currently translated at 99.3% (820 of 825 strings)

Co-authored-by: Translator <kvb@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-06-18 22:19:13 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
fd56559cc8 Translated using Weblate (Malay)
Currently translated at 100.0% (825 of 825 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
2023-06-18 22:19:13 +02:00
rubenwardy
3a70d6d188 Change unsupported game text 2023-06-18 21:18:42 +01:00
rubenwardy
c498818e8b Add supports_all_games to make game support explicit
Fixes #388 and fixes #441
2023-06-18 21:12:14 +01:00
rubenwardy
cb352fad47 Stats: Make date range dropdown blue when active 2023-06-15 09:00:11 +01:00
rubenwardy
2596253535 Stats: Improve summaries when range is selected
Fixes #446
2023-06-15 08:45:28 +01:00
rubenwardy
81651aee97 Stats: Update date range options 2023-06-14 23:00:22 +01:00
rubenwardy
80c42637df Stats: Add ability to select date range 2023-06-14 22:47:08 +01:00
rubenwardy
516361345e Stats: Fix annotation being added when outside graph range 2023-06-14 22:41:02 +01:00
rubenwardy
7f3b24a650 Fix typo on support packages page 2023-06-13 17:46:04 +01:00
rubenwardy
d443945b5c Fix release min/max error being shown incorrectly 2023-06-05 17:55:20 +01:00
rubenwardy
661d66cafb Update translations 2023-06-01 18:12:29 +01:00
Sharpik
3d8fdd70b3 Translated using Weblate (Czech)
Currently translated at 37.3% (307 of 821 strings)

Co-authored-by: Sharpik <david.maliska@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2023-06-01 19:11:48 +02:00
Giov4
9fcd2b7908 Translated using Weblate (Italian)
Currently translated at 100.0% (821 of 821 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-06-01 19:11:48 +02:00
nyommer
7e47730c8b Translated using Weblate (Hungarian)
Currently translated at 38.7% (318 of 821 strings)

Co-authored-by: nyommer <jishnu.ifeoluwa@fullangle.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2023-06-01 19:11:48 +02:00
José Muñoz
f368734241 Translated using Weblate (Spanish)
Currently translated at 90.0% (739 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 88.6% (728 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 86.3% (709 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 85.9% (706 of 821 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-06-01 19:11:48 +02:00
rubenwardy
28b3084186 Add missing CSS for client preview 2023-06-01 18:02:26 +01:00
rubenwardy
3375276f0d Show all featured packages in client 2023-06-01 18:00:08 +01:00
rubenwardy
0a77a0110d Split Featured tag into Featured and Spotlight 2023-06-01 17:55:00 +01:00
rubenwardy
78b5986027 Add client preview to screenshots page 2023-06-01 17:48:59 +01:00
rubenwardy
26abe9275c Promote featured packages in the client 2023-05-31 17:29:20 +01:00
rubenwardy
a0491216b9 Fix normalize_whitespace double escaping text 2023-05-27 15:28:06 +01:00
rubenwardy
e5f669ccb6 Revert "Birthday" and "Add birthday number and link to forum topic"
This reverts commit 224fef683d.

This reverts commit 14e01c9007.
2023-05-26 21:40:09 +01:00
rubenwardy
224fef683d Add birthday number and link to forum topic 2023-05-25 12:54:59 +01:00
rubenwardy
14e01c9007 Birthday 2023-05-25 12:29:18 +01:00
rubenwardy
bb206da804 Enable Turkish translation 2023-05-24 00:11:30 +01:00
Furkan Baytekin
d01391325e Translated using Weblate (Turkish)
Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Turkish)

Currently translated at 38.8% (319 of 821 strings)

Co-authored-by: Furkan Baytekin <furkanbaytekin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2023-05-23 00:48:38 +02:00
Giov4
7fa18e59e5 Translated using Weblate (Italian)
Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (821 of 821 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-05-23 00:48:38 +02:00
José Muñoz
6c1a97be1b Translated using Weblate (Spanish)
Currently translated at 84.1% (691 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 77.7% (638 of 821 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-05-23 00:48:37 +02:00
rubenwardy
996d46cad7 Fix all package stats API returning 0s 2023-05-20 00:56:44 +01:00
rubenwardy
8cde0cd852 Fix locked medal progress label being cut off
Fixes #365
2023-05-19 21:46:33 +01:00
rubenwardy
287aae8bd2 Disable linkify on <code> tags 2023-05-19 20:36:51 +01:00
rubenwardy
ff23f981e2 Disable linkify on text without https:// or http://
Fixes #374
2023-05-19 20:15:00 +01:00
rubenwardy
05bfa11d71 Fix test data failing txp constraint 2023-05-19 20:06:50 +01:00
rubenwardy
81b9833a81 Disable sqlalchemy-searchable sync_trigger to fix failing CI
This is a big hack and will prevent search working on databases that
haven't set it up yet, but is needed to get UI tests and such working.
2023-05-19 20:06:50 +01:00
rubenwardy
57b736b1df Todo: Add game support status 2023-05-19 19:40:03 +01:00
rubenwardy
d0f6be6826 Downgrade Flask and Werkzeug 2023-05-19 19:28:48 +01:00
rubenwardy
723994322b Refactor todo blueprint 2023-05-19 19:19:47 +01:00
rubenwardy
d8fa3342cf Discord webhooks: Fix understood failing status codes 2023-05-14 17:23:55 +01:00
rubenwardy
a5e258f7fa Fix throwing exception 2023-05-14 17:22:57 +01:00
rubenwardy
8178232911 Fix Discord webhooks failing due to avatar URL being relative 2023-05-14 17:21:23 +01:00
rubenwardy
1a173153c8 Inspect Discord webhook response 2023-05-14 17:16:56 +01:00
rubenwardy
adbbaf93c6 Fix session.execute in integration test utils 2023-05-13 19:12:52 +01:00
rubenwardy
fe64f15949 Fix stylesheet not being included 2023-05-13 18:21:13 +01:00
rubenwardy
1a8b6a23dd Fix error in old migrations caused by dependency updates 2023-05-13 17:53:21 +01:00
rubenwardy
286a598c77 Fix empty descriptions being added 2023-05-13 17:45:22 +01:00
rubenwardy
0c1d1354cb Fix name in README.md 2023-05-13 17:44:09 +01:00
Pexauteau Santander
4785ca1acc Translated using Weblate (Slovak)
Currently translated at 100.0% (821 of 821 strings)

Co-authored-by: Pexauteau Santander <pexauteau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Minetest/ContentDB
2023-05-13 18:32:15 +02:00
José Muñoz
8b3b8dccf4 Translated using Weblate (Spanish)
Currently translated at 73.0% (600 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 68.4% (562 of 821 strings)

Translated using Weblate (Spanish)

Currently translated at 64.5% (530 of 821 strings)

Co-authored-by: José Muñoz <dr.cabra@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-05-13 18:32:15 +02:00
John Donne
acd69b1d4d Translated using Weblate (French)
Currently translated at 99.6% (818 of 821 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-05-13 18:32:15 +02:00
Giov4
2b69f2e6ac Translated using Weblate (Italian)
Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (821 of 821 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-05-13 18:32:15 +02:00
Linerly
ef8e3451d6 Translated using Weblate (Indonesian)
Currently translated at 100.0% (821 of 821 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-05-13 18:32:15 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
b4a59cf135 Translated using Weblate (Malay)
Currently translated at 100.0% (821 of 821 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
2023-05-13 18:32:15 +02:00
rubenwardy
5ffc5fe341 Update Werkzeug 2023-05-13 17:05:31 +01:00
rubenwardy
b310cb3c80 Default to 4 workers 2023-05-13 16:55:27 +01:00
rubenwardy
08ff5199e3 Allow editors to use zipgrep 2023-05-13 16:48:48 +01:00
dependabot[bot]
70e46139cc Bump flask from 2.2.3 to 2.3.2
Bumps [flask](https://github.com/pallets/flask) from 2.2.3 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.2.3...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-13 16:19:33 +01:00
rubenwardy
e168da4f72 Fix unapproved games showing in supported games
Fixes #429
2023-05-13 16:10:53 +01:00
rubenwardy
0658368aad Homepage: Fix tag counts including unapproved packages 2023-05-13 15:40:03 +01:00
rubenwardy
1659802ff3 Fix tag counts including unapproved packages 2023-05-13 15:34:20 +01:00
rubenwardy
35afd50f3d Fix crash when generating diff on new package 2023-05-12 15:53:06 +01:00
rubenwardy
dee9959fda Remove unused admin actions 2023-05-12 01:13:09 +01:00
rubenwardy
ed8ce8c16c Add link to search from tag edit page 2023-05-12 01:08:22 +01:00
rubenwardy
7df1b8cc0f Remove old restore method 2023-05-12 01:06:09 +01:00
rubenwardy
e88ead392b Add audit logging to type editors 2023-05-12 01:03:21 +01:00
rubenwardy
f03746f5ad Allow editors and approvers to see package audit log descriptions 2023-05-12 00:55:44 +01:00
rubenwardy
84d379d490 Fix incorrect difference detected due to order of tags 2023-05-12 00:45:40 +01:00
rubenwardy
9738a8a826 describe_difference: Limit string field diff length 2023-05-12 00:20:38 +01:00
rubenwardy
19fa91d319 Add changes to edit package audit log entry 2023-05-12 00:17:15 +01:00
rubenwardy
1fc4852e77 Add check constraint to validate texture pack licenses 2023-05-11 23:08:01 +01:00
rubenwardy
a2a38f1e14 Add descriptions to about/faq flatpages 2023-05-08 01:49:23 +01:00
rubenwardy
fb329cd76e Link to Minetest Modding Book in help pages 2023-05-08 01:45:36 +01:00
rubenwardy
3ccb165522 Import forum profile pictures and host them directly 2023-05-05 18:43:20 +01:00
rubenwardy
a026e2c2bb Fix release creation using API 2023-05-02 10:24:52 +01:00
rubenwardy
d8ee237b1d Add og:title 2023-04-30 23:22:10 +01:00
rubenwardy
50037f6cb7 Remove remaining rubenwardy.com link in footer 2023-04-30 18:15:10 +01:00
rubenwardy
05486e53e0 Update translations
Fixes #435
2023-04-30 01:44:12 +01:00
rubenwardy
9fa42df385 Improve description tag generation 2023-04-30 00:54:01 +01:00
rubenwardy
c2ab4ac308 Add meta tag to donate page 2023-04-30 00:33:20 +01:00
rubenwardy
4c66b25e7c Fix crash on donate page 2023-04-30 00:27:10 +01:00
jolesh
b1570d4632 Translated using Weblate (Esperanto)
Currently translated at 19.8% (162 of 815 strings)

Co-authored-by: jolesh <jolesh0815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translation: Minetest/ContentDB
2023-04-26 14:47:38 +02:00
John Donne
0258bc7949 Translated using Weblate (French)
Currently translated at 99.6% (812 of 815 strings)

Co-authored-by: John Donne <akheron@zaclys.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-04-26 14:47:37 +02:00
Giov4
199dc6f59e Translated using Weblate (Italian)
Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-04-26 14:47:37 +02:00
syl
726204763d Translated using Weblate (French)
Currently translated at 99.0% (807 of 815 strings)

Co-authored-by: syl <syl@gresille.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-04-26 14:47:37 +02:00
AFCMS
10eb23d760 Translated using Weblate (French)
Currently translated at 98.8% (806 of 815 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-04-26 14:47:36 +02:00
rubenwardy
b785a66ae8 Reduce number of indexed pages
Removes some package search pages from search results. Also removes all package thread lists (as they are redundant)
2023-04-24 00:17:26 +01:00
rubenwardy
d16969837c Add About page 2023-04-23 23:27:18 +01:00
rubenwardy
a5ec46f14c Update database dependencies 2023-04-23 21:51:51 +01:00
rubenwardy
660ef72532 Update non-database dependencies 2023-04-23 21:51:51 +01:00
rubenwardy
3c1ba226c4 FAQ: Clarify email verification account deletion 2023-04-23 16:36:33 +01:00
rubenwardy
2a0545210b hypertext: Add support for nested lists 2023-04-19 20:03:07 +01:00
rubenwardy
0a06e41497 Add API to provide descriptions as Minetest hypertext markup 2023-04-19 20:03:07 +01:00
rubenwardy
dfe829d59e Update some dependencies 2023-04-19 02:59:57 +01:00
rubenwardy
64280bd960 Add rules for user behaviour 2023-04-19 02:28:30 +01:00
rubenwardy
a97da15359 Don't consider neutral reviews to be positive 2023-04-15 20:06:24 +01:00
rubenwardy
c9e8df7f5b Simplify review rating 2023-04-15 04:02:09 +01:00
rubenwardy
1b1955f052 Fix review rating 2023-04-15 03:20:01 +01:00
rubenwardy
b1bd39c0fc Add ability to make neutral reviews 2023-04-15 02:46:35 +01:00
rubenwardy
1235bc14db Fix account deactivation 2023-04-03 20:43:26 +01:00
Fábio Rodrigues Ribeiro
5cbfe4cda5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 5.3% (44 of 815 strings)

Co-authored-by: Fábio Rodrigues Ribeiro <farribeiro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2023-04-02 19:57:03 +02:00
Giov4
4f38b77107 Translated using Weblate (Italian)
Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-04-02 19:57:03 +02:00
rubenwardy
634fafc880 Enable Ukrainian 2023-03-29 16:15:23 +01:00
Giov4
671b975a68 Translated using Weblate (Italian)
Currently translated at 99.3% (810 of 815 strings)

Co-authored-by: Giov4 <brancacciogiovanni1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2023-03-29 17:06:21 +02:00
Денис Савченко
f51541ffe3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Денис Савченко <denissavchenko0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2023-03-29 17:06:21 +02:00
Muhammad Rifqi Priyo Susanto
034a024cb2 Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-03-29 17:06:21 +02:00
rubenwardy
766765b1f8 Fix crash on unimplemented is_
Whilst is_ is documented, it appears to be
unimplemented for certain objects.
2023-03-29 10:58:09 +01:00
rubenwardy
e7f5f7055d Remove accidental about link 2023-03-27 10:32:52 +01:00
rubenwardy
fd06d86062 Rename report link 2023-03-24 17:56:02 +00:00
ROllerozxa
a8ed6b5b44 Translated using Weblate (Swedish)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: ROllerozxa <rollerozxa@voxelmanip.se>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-03-19 00:48:39 +01:00
Translator
096efb8fa4 Translated using Weblate (French)
Currently translated at 98.7% (805 of 815 strings)

Co-authored-by: Translator <kvb@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-03-19 00:48:39 +01:00
ROllerozxa
ec6fc5236b Translated using Weblate (Swedish)
Currently translated at 98.2% (801 of 815 strings)

Co-authored-by: ROllerozxa <temporaryemail4meh+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-03-19 00:48:39 +01:00
rubenwardy
fb13272e6c Fix crash due to relations not supporting is_ 2023-03-18 23:20:09 +00:00
rubenwardy
7eca06a097 Fix web hooks updating deleted packages 2023-03-18 14:22:39 +00:00
Julien Maulny
32f353af8f Translated using Weblate (French)
Currently translated at 98.0% (799 of 815 strings)

Co-authored-by: Julien Maulny <julien.maulny@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-03-17 15:26:26 +01:00
Rodion Borisov
739e0eb316 Translated using Weblate (Russian)
Currently translated at 94.3% (769 of 815 strings)

Co-authored-by: Rodion Borisov <vintprox@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-03-17 15:26:26 +01:00
Wuzzy
13624a7a97 Translated using Weblate (German)
Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (German)

Currently translated at 99.1% (808 of 815 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-03-17 15:26:25 +01:00
Muhammad Rifqi Priyo Susanto
a2760da676 Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-03-17 15:26:25 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
9873e40076 Translated using Weblate (Malay)
Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (815 of 815 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
2023-03-17 15:26:24 +01:00
rubenwardy
a2c096a6e6 Update translations 2023-03-09 18:41:22 +00:00
Артём Котлубай
865eb0112d Translated using Weblate (Russian)
Currently translated at 96.1% (768 of 799 strings)

Co-authored-by: Артём Котлубай <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
Arsenicus
6ed7061bce Translated using Weblate (Russian)
Currently translated at 96.1% (768 of 799 strings)

Co-authored-by: Arsenicus <divided0303@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
Wuzzy
7636748289 Translated using Weblate (German)
Currently translated at 100.0% (799 of 799 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
chocomint
fdd9609557 Translated using Weblate (Spanish)
Currently translated at 63.8% (510 of 799 strings)

Co-authored-by: chocomint <silentxe1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
unacceptium core
40c3daf563 Translated using Weblate (Hungarian)
Currently translated at 38.1% (305 of 799 strings)

Co-authored-by: unacceptium core <kolonics20132a@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
Артём Котлубай
baf6a8e418 Translated using Weblate (Russian)
Currently translated at 95.8% (766 of 799 strings)

Co-authored-by: Артём Котлубай <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
rubenwardy
49551a8a70 Translated using Weblate (Slovak)
Currently translated at 100.0% (799 of 799 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
Pexauteau Santander
4839703389 Translated using Weblate (Slovak)
Currently translated at 100.0% (799 of 799 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (797 of 799 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (797 of 799 strings)

Co-authored-by: Pexauteau Santander <pexauteau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
Jakub Z
564eb4a85f Translated using Weblate (Polish)
Currently translated at 100.0% (799 of 799 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
ROllerozxa
6fe7df2233 Translated using Weblate (Swedish)
Currently translated at 100.0% (799 of 799 strings)

Co-authored-by: ROllerozxa <temporaryemail4meh+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-03-09 18:35:34 +00:00
rubenwardy
027a6a79bc Add description to donation page 2023-03-09 18:23:35 +00:00
rubenwardy
07f5d2e0d5 Add per-package donate URLs 2023-03-05 18:17:03 +00:00
rubenwardy
08054e4969 Add page to find packages asking for donations 2023-03-05 17:33:09 +00:00
Nicolae Crefelean
4e154644ee Show the image caption on screenshot hover (#423) 2023-03-05 17:33:09 +00:00
rubenwardy
e0f9623670 Fix wrong comment being used for reviews
Fixes #417
2023-01-28 16:39:53 +00:00
rubenwardy
37b200295c Fix crash on empty display name 2023-01-21 19:03:43 +00:00
rubenwardy
88022667ce Remove incorrect check on package provides 2023-01-14 23:54:45 +00:00
rubenwardy
c927a87db3 Fix long audit descriptions being lost 2023-01-13 23:49:06 +00:00
rubenwardy
157f418855 Fix game support not being in tabs for txp 2023-01-12 21:10:58 +00:00
rubenwardy
42b9986fc8 Enable game support for texture packs 2023-01-12 19:48:18 +00:00
rubenwardy
605015f812 Update blocked site message 2023-01-12 16:43:10 +00:00
Kristian
cc1eec93d5 Added translation using Weblate (Danish)
Co-authored-by: Kristian <macrofag@protonmail.com>
2023-01-12 13:55:19 +00:00
Jakub Z
32354483fc Translated using Weblate (Polish)
Currently translated at 93.2% (745 of 799 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2023-01-12 13:55:19 +00:00
Gao Tiesuan
46d4ca6b0f Translated using Weblate (Chinese (Simplified))
Currently translated at 96.8% (774 of 799 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2023-01-12 13:55:19 +00:00
AFCMS
ddac098704 Translated using Weblate (French)
Currently translated at 100.0% (799 of 799 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-01-12 13:55:19 +00:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
6d50546eba Translated using Weblate (Malay)
Currently translated at 100.0% (799 of 799 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
2023-01-12 13:55:19 +00:00
rubenwardy
14d29a54e1 Update AI section in copyright guide 2023-01-09 00:21:43 +00:00
rubenwardy
be8de45714 Fix game support crash due to incorrect key 2023-01-08 12:23:33 +00:00
rubenwardy
0a6e3237b1 Fix untranslatable strings 2023-01-08 12:23:33 +00:00
rubenwardy
008e6ba2e6 Add help page about copyright (#415) 2023-01-05 13:08:55 +00:00
rubenwardy
46b804834a Fix embed being required in webhook 2023-01-05 09:43:27 +00:00
rubenwardy
540e24e8f9 Add Discord embed to package webhooks 2023-01-04 20:58:32 +00:00
AFCMS
4c98063d6a Fix invalid API route in docs for .cdb.json schema (#416) 2023-01-03 12:43:18 +00:00
rubenwardy
72b4029ed3 Add ability to block domains 2023-01-03 12:17:25 +00:00
AFCMS
13dcd373f2 Add API endpoint for .cdb.json JSON Schema (#402)
Fixes #393
2023-01-02 19:26:10 +00:00
rubenwardy
65e8929689 Fix game support errors not showing to users
Fixes #394
2023-01-02 19:19:33 +00:00
rubenwardy
31a748b0b3 Fix deleted packaging being show on todo page
Fixes #401
2023-01-02 19:16:51 +00:00
rubenwardy
38baea3dcf Fix wrong scheme used when Git cloning
Fixes #408
2023-01-02 19:14:33 +00:00
rubenwardy
ad0e958736 Fix mention of content flags in non-free help 2023-01-02 17:41:10 +00:00
rubenwardy
c1600b90a6 Loop chart colors 2023-01-02 17:34:34 +00:00
rubenwardy
72d999e759 Remove login button from gone page when already logged in
Fixes #414
2023-01-02 17:25:08 +00:00
rubenwardy
a7bbb45fc2 Use 403 status code for unpublished pages 2023-01-02 17:20:08 +00:00
rubenwardy
34bbb281e0 Update translations 2023-01-02 16:19:42 +00:00
Артём Котлубай
4b61657602 Translated using Weblate (Russian)
Currently translated at 97.4% (768 of 788 strings)

Co-authored-by: Артём Котлубай <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Arsenicus
db7278664f Translated using Weblate (Russian)
Currently translated at 97.4% (768 of 788 strings)

Co-authored-by: Arsenicus <divided0303@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Worriz BANG-E 谨京
cd4fa81260 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.0% (741 of 788 strings)

Co-authored-by: Worriz BANG-E 谨京 <3263125443@qq.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
CouldBeMathijs
68fbbfa8d5 Translated using Weblate (Dutch)
Currently translated at 60.4% (476 of 788 strings)

Co-authored-by: CouldBeMathijs <mathijs.pittoors@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nl/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Fábio Rodrigues Ribeiro
a5c0a48d2b Translated using Weblate (Portuguese (Brazil))
Currently translated at 2.5% (20 of 788 strings)

Co-authored-by: Fábio Rodrigues Ribeiro <farribeiro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Wuzzy
89f08d4217 Translated using Weblate (German)
Currently translated at 100.0% (788 of 788 strings)

Translated using Weblate (German)

Currently translated at 94.6% (746 of 788 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Eduardo Lima
ba881ec2e1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 2.5% (20 of 788 strings)

Co-authored-by: Eduardo Lima <edu200399lim@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
ROllerozxa
717255bf50 Translated using Weblate (Swedish)
Currently translated at 100.0% (788 of 788 strings)

Co-authored-by: ROllerozxa <temporaryemail4meh+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2023-01-02 17:18:34 +01:00
Muhammad Rifqi Priyo Susanto
090883cb61 Translated using Weblate (Indonesian)
Currently translated at 100.0% (788 of 788 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2023-01-02 17:18:33 +01:00
rubenwardy
b102c41008 Translated using Weblate (French)
Currently translated at 93.7% (739 of 788 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2023-01-02 17:18:33 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
a0d0eedbb3 Translated using Weblate (Malay)
Currently translated at 100.0% (788 of 788 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
2023-01-02 17:18:33 +01:00
rubenwardy
16abb636c5 Show message on draft packages rather than 404 2023-01-02 16:17:28 +00:00
rubenwardy
a7e6f19ae6 Move package approval section above header
In an attempt to make it more obvious
2023-01-02 16:14:20 +00:00
rubenwardy
18f70738d0 Prevent reviewing unapproved packages 2023-01-02 15:51:19 +00:00
wsor4035
d3bdf4cf03 Remove white background from avatars
Fixes #387
2023-01-01 22:48:10 +00:00
rubenwardy
5e425cd29c Validate mod directory casing 2023-01-01 22:45:10 +00:00
rubenwardy
048b02db1d Update copyright year 2023-01-01 21:09:59 +00:00
rubenwardy
8930418d53 Update gone page message 2023-01-01 20:42:40 +00:00
rubenwardy
8ef737b16c Fix being able to see unpublished packages 2023-01-01 20:39:35 +00:00
wsor4035
01344256a9 Fix license editor crash (#410) 2022-12-26 19:01:34 +00:00
rubenwardy
5940919fae Add docs for game Package Query arg 2022-12-20 13:48:42 +00:00
rubenwardy
c4ccd82f63 Remove password suggestions
It was based on 5 words from a 2048 word list, which is only the same as a 11 character password
2022-12-15 16:04:02 +00:00
rubenwardy
a669131178 Update translations 2022-12-09 19:07:30 +00:00
runs
1a859cf341 Translated using Weblate (Spanish)
Currently translated at 67.6% (505 of 746 strings)

Co-authored-by: runs <runspect@yahoo.es>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-12-09 20:05:46 +01:00
Gao Tiesuan
067e0dba91 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.5% (743 of 746 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-12-09 20:05:46 +01:00
Muhammad Rifqi Priyo Susanto
d5413cc751 Translated using Weblate (Indonesian)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-12-09 20:05:46 +01:00
Темак
cd7cdbcf72 Translated using Weblate (Russian)
Currently translated at 97.3% (726 of 746 strings)

Co-authored-by: Темак <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-12-09 20:05:46 +01:00
Nyuh Nyash
ae15c5ebe6 Translated using Weblate (Russian)
Currently translated at 97.3% (726 of 746 strings)

Co-authored-by: Nyuh Nyash <egor.kuryasev@ya.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-12-09 20:05:46 +01:00
rubenwardy
a7b6778f64 Remove gamejam promotion 2022-12-04 15:02:49 +00:00
rubenwardy
7558346071 Don't allow users to recreate removed packages 2022-11-23 19:13:03 +00:00
rubenwardy
f986caf18b Allow new members to create API tokens 2022-11-22 21:56:19 +00:00
rubenwardy
ba7ed40d6a Fix crash in upgrade_new_members task 2022-11-22 21:55:40 +00:00
rubenwardy
4435679737 Update text on claim page 2022-11-22 21:52:02 +00:00
rubenwardy
34aaa45c92 Fix failing test 2022-11-18 21:37:59 +00:00
rubenwardy
a81d289bc8 Add caching headers to API 2022-11-18 21:36:15 +00:00
rubenwardy
db8574ffe3 Homepage: Preload review information in queries 2022-11-18 21:15:46 +00:00
rubenwardy
b72244398b Homepage: Preload package authors and screenshots in queries 2022-11-18 20:29:52 +00:00
rubenwardy
01bc519b86 Add API to get all package stats 2022-11-15 01:51:21 +00:00
rubenwardy
292b4f5483 Statistics: Rotate annotation and add shadow 2022-11-11 01:18:31 +00:00
rubenwardy
6212b95a4d Statistics: Add label for Nov5 YouTube event 2022-11-11 01:05:27 +00:00
rubenwardy
6916b0612f Add redirect to current_user's stats 2022-11-09 20:52:31 +00:00
rubenwardy
1f40a5bf8b Add user info API 2022-11-09 20:47:19 +00:00
rubenwardy
b370b3bab2 Add statistics link to user dropdown in navbar 2022-11-09 20:09:40 +00:00
rubenwardy
9f375c6235 Fix crash on user stats page with no packages 2022-11-09 20:08:50 +00:00
rubenwardy
c9b5e3374c Remove limit on packages in chart 2022-11-09 19:46:42 +00:00
rubenwardy
31aef061fb Add downloads by package graph 2022-11-09 19:45:25 +00:00
rubenwardy
f7742d47ff Add package dropdown to statistics pages 2022-11-09 18:58:58 +00:00
rubenwardy
724b80e91e Add user statistics pages 2022-11-09 18:47:44 +00:00
rubenwardy
82cd0aefdf Attempt to fix chart stretching 2022-11-09 18:46:42 +00:00
rubenwardy
e15a3c682f Statistics: Fix issue with missing days
Fixes #397
2022-11-09 18:46:11 +00:00
rubenwardy
852e6ab5a0 Fix crash in phpbbparser 2022-11-09 17:41:32 +00:00
rubenwardy
20bf16abbf Statistics: Add .csv download 2022-11-08 18:47:28 +00:00
rubenwardy
5fc603682d Statistics: Add weekly and monthly total/average 2022-11-08 18:47:03 +00:00
rubenwardy
977fc1ce96 Move statistics link on package page 2022-11-07 01:41:13 +00:00
rubenwardy
f547fd258d Fix squashed charts 2022-11-07 00:53:30 +00:00
rubenwardy
b0cece3f5f Add empty view to stats page 2022-11-06 19:47:42 +00:00
rubenwardy
53601b77c8 Adjust score given on downloads 2022-11-06 19:44:36 +00:00
rubenwardy
0cb220acff Use UTC dates for stats 2022-11-06 19:12:43 +00:00
rubenwardy
aa2996f92e Fix blocked JS by renaming _stats.js to _charts.js 2022-11-06 19:00:34 +00:00
rubenwardy
69662eeafc Stats: Improve page 2022-11-06 18:51:33 +00:00
rubenwardy
4387e71417 Add statistics page 2022-11-06 18:02:54 +00:00
rubenwardy
5c0480b39d Add script to import stats from nginx logs 2022-11-06 16:54:06 +00:00
rubenwardy
e1f4787fb9 Add package stats endpoint 2022-11-06 10:32:46 +00:00
rubenwardy
de70b21e55 Fix PackageDailyStats field names 2022-11-05 23:22:21 +00:00
rubenwardy
d11d638144 Add per-package download tracking 2022-11-05 22:43:57 +00:00
Kisjuhász Attila
02ef7e09e4 Translated using Weblate (Hungarian)
Currently translated at 27.4% (205 of 746 strings)

Co-authored-by: Kisjuhász Attila <kj.attil@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2022-11-01 12:22:13 +01:00
rubenwardy
f010a12ded Improve package search title generation 2022-10-28 16:21:31 +01:00
rubenwardy
e50f7094f8 Replace game jam banner 2022-10-26 09:43:23 +01:00
rubenwardy
ac3047f124 Add game jam 2022 ad 2022-10-25 17:24:43 +01:00
rubenwardy
294037ec70 Use level 4 thumbnail for cover image 2022-10-25 02:00:12 +01:00
rubenwardy
5a506ef557 Add level 4 thumbnails 2022-10-25 01:49:06 +01:00
rubenwardy
da3af2c22f Only allow editors to access user comments page 2022-10-24 14:43:39 +01:00
rubenwardy
1ef71b7a59 Add robots.txt to git 2022-10-19 15:08:27 +01:00
rubenwardy
51d2b82acf Add page to list mods that don't support current version 2022-10-15 12:53:17 +01:00
rubenwardy
5f42e35231 Fix crash on sign up 2022-10-14 12:52:29 +01:00
rubenwardy
e2af9893ce Fix failing unit test 2022-10-13 19:27:18 +01:00
rubenwardy
cb443a2d15 Add game.conf example to package config help 2022-10-13 19:23:45 +01:00
rubenwardy
a7f2cc5d2b Disallow usernames only containing "." 2022-10-13 19:23:18 +01:00
rubenwardy
905185812b Fix broken search input-group 2022-10-13 19:21:16 +01:00
rubenwardy
5baa06d8c3 Use icon for search button rather than text 2022-10-13 17:21:31 +01:00
rubenwardy
cbd430841c Move Threads to footer from navbar 2022-10-13 16:58:12 +01:00
rubenwardy
bfe32d8fc8 Enable Italian and Vietnamese 2022-10-13 16:56:59 +01:00
Темак
a2509df38b Translated using Weblate (Russian)
Currently translated at 97.3% (726 of 746 strings)

Co-authored-by: Темак <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-10-13 17:38:51 +02:00
Nikita Epifanov
a0a4dc2cfa Translated using Weblate (Russian)
Currently translated at 97.3% (726 of 746 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-10-13 17:38:51 +02:00
Yic95
3fe4eae4c0 Translated using Weblate (Chinese (Traditional))
Currently translated at 13.0% (97 of 746 strings)

Co-authored-by: Yic95 <0Luke.Luke0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2022-10-13 17:38:51 +02:00
Victor K
783611350c Translated using Weblate (Russian)
Currently translated at 96.7% (722 of 746 strings)

Co-authored-by: Victor K <ktrace@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-10-13 17:38:50 +02:00
Matheus Bastos
18daff762f Translated using Weblate (Portuguese (Brazil))
Currently translated at 1.4% (11 of 746 strings)

Added translation using Weblate (Portuguese (Brazil))

Co-authored-by: Matheus Bastos <matheusmcy1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pt_BR/
Translation: Minetest/ContentDB
2022-10-13 17:38:50 +02:00
Văn Chí
662aed13ad Translated using Weblate (Vietnamese)
Currently translated at 93.6% (699 of 746 strings)

Added translation using Weblate (Vietnamese)

Co-authored-by: Văn Chí <chiv8331@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/vi/
Translation: Minetest/ContentDB
2022-10-13 17:38:50 +02:00
Muhammad Rifqi Priyo Susanto
55748a24b1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-10-13 17:38:49 +02:00
BRN Systems
61128fd054 Translated using Weblate (Slovak)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: BRN Systems <brnsystems123@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
Translation: Minetest/ContentDB
2022-10-13 17:38:49 +02:00
Andrei Stepanov
febe66089c Translated using Weblate (Russian)
Currently translated at 96.6% (721 of 746 strings)

Co-authored-by: Andrei Stepanov <adem4ik@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-10-13 17:38:48 +02:00
Andrés Morgensen
aae512469f Translated using Weblate (Spanish)
Currently translated at 67.0% (500 of 746 strings)

Co-authored-by: Andrés Morgensen <expressindia_aeromax@slmail.me>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-10-13 17:38:48 +02:00
Wuzzy
b75f321094 Translated using Weblate (German)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-10-13 17:38:48 +02:00
Fjuro
613394c342 Translated using Weblate (Czech)
Currently translated at 6.7% (50 of 746 strings)

Added translation using Weblate (Czech)

Co-authored-by: Fjuro <fjuro@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/cs/
Translation: Minetest/ContentDB
2022-10-13 17:38:47 +02:00
Valentino
ca1f935b18 Translated using Weblate (Spanish)
Currently translated at 65.2% (487 of 746 strings)

Co-authored-by: Valentino <phamtomwhite@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-10-13 17:38:47 +02:00
Lemente
5083dbf543 Translated using Weblate (French)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-10-13 17:38:47 +02:00
ROllerozxa
66bbb92c1f Translated using Weblate (Swedish)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: ROllerozxa <temporaryemail4meh+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2022-10-13 17:38:46 +02:00
Niklp
2ee720485f Translated using Weblate (German)
Currently translated at 97.3% (726 of 746 strings)

Co-authored-by: Niklp <ngs.nik@outlook.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-10-13 17:38:46 +02:00
Farooq Karimi Zadeh
3f9d9c5d65 Translated using Weblate (Persian)
Currently translated at 11.2% (84 of 746 strings)

Translated using Weblate (Persian)

Currently translated at 8.9% (67 of 746 strings)

Added translation using Weblate (Persian)

Co-authored-by: Farooq Karimi Zadeh <fkz@riseup.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fa/
Translation: Minetest/ContentDB
2022-10-13 17:38:46 +02:00
Cow Boy
5b7a19ff2c Translated using Weblate (Latvian)
Currently translated at 35.7% (267 of 746 strings)

Added translation using Weblate (Latvian)

Co-authored-by: Cow Boy <cowboylv@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/lv/
Translation: Minetest/ContentDB
2022-10-13 17:38:45 +02:00
Tom Schmelzer
3646a44f93 Translated using Weblate (German)
Currently translated at 96.9% (723 of 746 strings)

Co-authored-by: Tom Schmelzer <tom.schmelzer@web.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-10-13 17:38:45 +02:00
Темак
e7335b514a Translated using Weblate (Russian)
Currently translated at 95.8% (715 of 746 strings)

Co-authored-by: Темак <artemkotlubai@yandex.ru>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-10-13 17:38:44 +02:00
Gao Tiesuan
89dd4f8d08 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Esperanto)

Currently translated at 3.7% (28 of 746 strings)

Added translation using Weblate (Esperanto)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/eo/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-10-13 17:38:44 +02:00
Pietro Cappuccino
e2fd2fe78b Translated using Weblate (Italian)
Currently translated at 100.0% (746 of 746 strings)

Added translation using Weblate (Italian)

Co-authored-by: Pietro Cappuccino <p.cappuccino@tiscali.it>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/it/
Translation: Minetest/ContentDB
2022-10-13 17:38:43 +02:00
Jakub Z
49aeede0f6 Translated using Weblate (Polish)
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2022-10-13 17:38:43 +02:00
Miniontoby
6511c358c8 Translated using Weblate (Dutch)
Currently translated at 13.6% (102 of 746 strings)

Co-authored-by: Miniontoby <tobias.gaarenstroom@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nl/
Translation: Minetest/ContentDB
2022-10-13 17:38:43 +02:00
Maxime Leroy
4b5156f168 Translated using Weblate (French)
Currently translated at 99.7% (744 of 746 strings)

Translated using Weblate (French)

Currently translated at 99.3% (741 of 746 strings)

Co-authored-by: Maxime Leroy <lisacintosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-10-13 17:38:42 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
9b3ca4644a Translated using Weblate (Malay)
Currently translated at 100.0% (746 of 746 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
2022-10-13 17:38:42 +02:00
qwerty123a2
6edc4645d2 Makes it clear that an error occurred. (#390)
Co-authored-by: rubenwardy <rw@rubenwardy.com>
2022-09-25 15:35:23 +01:00
rubenwardy
90710cc8b9 Remove full description from search index 2022-09-14 22:12:43 +01:00
rubenwardy
7bfb183578 Fix broken metapackages -> modnames redirect 2022-09-01 23:14:56 +01:00
rubenwardy
906ec3885a Rename "Metapackage" to "Mod Name" 2022-09-01 22:56:49 +01:00
rubenwardy
f649fa57e6 Move mods above games in metapackage page 2022-09-01 22:30:43 +01:00
rubenwardy
b4208f2dda Reintroduce New Member rank
Fixes #183
2022-08-23 02:31:17 +01:00
rubenwardy
1d36f7d12b Update package config doc 2022-08-19 00:06:54 +01:00
rubenwardy
631ef2b10a Add migration to fix search weights 2022-08-18 16:44:40 +01:00
rubenwardy
bae1df2e8d Rename fileUpload -> file_upload 2022-08-09 17:51:17 +01:00
rubenwardy
0b92d43871 Add missing screenshots page to editor console 2022-08-09 13:48:49 +01:00
rubenwardy
9b7f1e6e88 Add missing screenshots item to user todo 2022-08-09 13:39:58 +01:00
wsor4035
c0447cdcd2 Add Editor crash course (#385)
Co-authored-by: rubenwardy <rw@rubenwardy.com>
2022-08-01 13:02:16 +01:00
rubenwardy
3be937c503 Remove zipgrep min length 2022-07-25 18:05:14 +01:00
rubenwardy
bc4e83d76a Add noindex to report page when given a URL 2022-07-19 23:29:56 +01:00
rubenwardy
20411e6f81 Make reporting the report page a 404 2022-07-19 23:20:13 +01:00
rubenwardy
56298ed57f Add graceful error message for invalid json files 2022-06-25 15:36:59 +01:00
dependabot[bot]
ec8dcf5960 Bump pillow from 9.0.0 to 9.0.1 (#373)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.0 to 9.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.0.0...9.0.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-25 15:31:19 +01:00
rubenwardy
294a968c9f Fix crash when null is used for an array in .cdb.json
Fixes #298
2022-06-25 15:27:40 +01:00
rubenwardy
78717b5eea Move game support updateAll to celery task 2022-06-25 04:47:38 +01:00
rubenwardy
e86eb6a4b8 Update game support blacklist 2022-06-25 04:39:07 +01:00
rubenwardy
a8de369edf Disallow "_game" as a package name 2022-06-25 03:44:25 +01:00
rubenwardy
3b140df508 Improve bot message friendliness 2022-06-25 03:30:12 +01:00
rubenwardy
cef8985d38 Make editor GUI override author game support 2022-06-25 03:20:27 +01:00
rubenwardy
4fdfc49429 Add mod.conf example to supported games page 2022-06-25 03:13:36 +01:00
rubenwardy
bfbab7ae9e Prevent disabling game detection without manually specifying games 2022-06-25 02:51:29 +01:00
rubenwardy
e091bd6fb0 Use db.session for GameSupportResolver in postReleaseCheckUpdate 2022-06-25 02:41:50 +01:00
rubenwardy
4a82172e07 Fix detached instance error in game support 2022-06-25 02:39:36 +01:00
rubenwardy
d9e65f7c3a Add option to disable game support detection 2022-06-25 02:27:51 +01:00
rubenwardy
42841896d1 Add ability for editors to set game support 2022-06-25 01:17:20 +01:00
rubenwardy
7f00b77db3 Add helpful page for game support 2022-06-25 00:32:32 +01:00
rubenwardy
d6887d7b46 Add support for un/"supported_games" in mod.conf 2022-06-24 23:56:27 +01:00
rubenwardy
f22911b4a0 Fix and reenable game support 2022-06-24 23:18:16 +01:00
ROllerozxa
4adb209894 Fix Swedish language code (#380) 2022-06-20 15:38:49 +01:00
rubenwardy
fa55f1d03b Report: Fix URL 2022-06-13 17:12:30 +01:00
rubenwardy
f511771fd4 Remove unused field 2022-06-13 17:11:50 +01:00
rubenwardy
5bd6ab7611 Disable reports from anonymous users 2022-06-13 17:10:14 +01:00
rubenwardy
f5643173a8 Enable Swedish 2022-06-05 18:17:52 +01:00
rubenwardy
8f3ebd182c Update translations 2022-06-05 18:10:53 +01:00
Miniontoby
8235d8390d Translated using Weblate (Dutch)
Currently translated at 14.2% (105 of 735 strings)

Added translation using Weblate (Dutch)

Co-authored-by: Miniontoby <tobias.gaarenstroom@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nl/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Wuzzy
878441406d Translated using Weblate (German)
Currently translated at 100.0% (735 of 735 strings)

Co-authored-by: Wuzzy <Wuzzy@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Jakub Z
9b0868c255 Translated using Weblate (Polish)
Currently translated at 100.0% (735 of 735 strings)

Co-authored-by: Jakub Z <mrkubax10@onet.pl>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Covarubio
21615ad35c Translated using Weblate (Russian)
Currently translated at 95.9% (705 of 735 strings)

Co-authored-by: Covarubio <6amffsl@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
GT-610
de38bc1557 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (735 of 735 strings)

Co-authored-by: GT-610 <myddz1005@163.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Linerly
2d1411e785 Translated using Weblate (Indonesian)
Currently translated at 100.0% (735 of 735 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Muhammad Rifqi Priyo Susanto
abe44d02fb Translated using Weblate (Indonesian)
Currently translated at 99.8% (734 of 735 strings)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Raquel Fariña Agra
b844c1f8d9 Translated using Weblate (Galician)
Currently translated at 5.9% (44 of 735 strings)

Added translation using Weblate (Galician)

Co-authored-by: Raquel Fariña Agra <raquelagra1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/gl/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
Maxime Leroy
5952b2a34a Translated using Weblate (French)
Currently translated at 99.5% (732 of 735 strings)

Co-authored-by: Maxime Leroy <lisacintosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
3raven
b373cfed96 Translated using Weblate (French)
Currently translated at 99.5% (732 of 735 strings)

Co-authored-by: 3raven <elise_declerck@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-06-05 19:09:10 +02:00
ROllerozxa
4e03636588 Translated using Weblate (Swedish)
Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Swedish)

Currently translated at 83.4% (613 of 735 strings)

Co-authored-by: ROllerozxa <temporaryemail4meh+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
Translation: Minetest/ContentDB
2022-06-05 19:09:09 +02:00
Yic95
651174bcb8 Translated using Weblate (Chinese (Traditional))
Currently translated at 8.2% (61 of 735 strings)

Co-authored-by: Yic95 <0Luke.Luke0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2022-06-05 19:09:09 +02:00
AFCMS
b1f6f1ea99 Translated using Weblate (French)
Currently translated at 98.7% (726 of 735 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-06-05 19:09:09 +02:00
Gao Tiesuan
e91860cadf Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (735 of 735 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-06-05 19:09:09 +02:00
rubenwardy
6375cf7ae8 Allow modpacks with only a single mod 2022-06-05 17:59:25 +01:00
rubenwardy
2a82c08d8b Update wording on screenshot policy 2022-06-05 01:44:44 +01:00
rubenwardy
0a89849157 Improve username error messages and duplicate messages 2022-06-05 01:00:45 +01:00
rubenwardy
adaf44bc2b Fix package deletion
Fixes #192
2022-06-01 17:30:28 +01:00
rubenwardy
8b5d767d3c Fix issues with deleted packages 2022-06-01 17:30:28 +01:00
rubenwardy
767bc9ef12 Add placeholder.png license 2022-05-20 18:05:45 +01:00
rubenwardy
dfc0af21ee Lower ratelimit 2022-05-09 12:40:01 +01:00
rubenwardy
cfd67dce33 Add ratelimit to package reviews 2022-05-08 16:01:13 +01:00
rubenwardy
0241c51f6f Fix thread template styling issue 2022-04-26 22:50:14 +01:00
rubenwardy
958020b19b Fix new thread private behaviour 2022-04-23 21:44:27 +01:00
rubenwardy
34d66a3d96 Update thread visibility guidance 2022-04-23 21:38:16 +01:00
rubenwardy
0689565ded Fix crash on mentioning user 2022-04-23 21:31:59 +01:00
rubenwardy
8fcbdd0666 Add mentioned users to thread when replying too 2022-04-23 21:20:36 +01:00
rubenwardy
c7d251b206 Add New Thread button to threads list 2022-04-23 21:17:03 +01:00
rubenwardy
f3ff44203c Add is_status_update to thread replies 2022-04-23 20:56:50 +01:00
rubenwardy
ee2311025c Allow watchers to see private threads, add list of users able to see thread 2022-04-23 20:42:58 +01:00
rubenwardy
b8e40b166d Fix comments being lost when two users try to open a package approval thread 2022-04-23 20:22:49 +01:00
rubenwardy
d7dd0274fa Fix long comments being swallowed 2022-04-23 20:05:19 +01:00
rubenwardy
b67e9a8130 Improve screenshots policy formatting 2022-04-23 14:59:54 +01:00
rubenwardy
794d113ce9 Fix list formatting in policy and guidance 2022-04-23 14:39:07 +01:00
rubenwardy
d8336989a8 Add policy on screenshots
Fixes #333
2022-04-23 14:29:28 +01:00
rubenwardy
64acf3047f Fix minor issues with zipgrep 2022-04-09 01:41:38 +01:00
rubenwardy
f137dfa978 Enable Polish, Chinese, and Slovak 2022-04-09 01:41:18 +01:00
ROllerozxa
cb25e5e6d8 Translated using Weblate (Swedish)
Currently translated at 81.7% (601 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
2022-04-06 10:07:02 +02:00
Linerly
acdeaf19cf Translated using Weblate (Indonesian)
Currently translated at 86.2% (634 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
2022-04-06 10:07:02 +02:00
Pexauteau Santander
d30b907a8a Translated using Weblate (Slovak)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sk/
2022-04-06 10:07:02 +02:00
Pexauteau Santander
76fbe00361 Added translation using Weblate (Slovak) 2022-04-06 10:07:02 +02:00
Andrij Mizyk
6a0c48e3d6 Translated using Weblate (Ukrainian)
Currently translated at 32.2% (237 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
2022-04-06 10:07:02 +02:00
Gao Tiesuan
a0e016a9e5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
2022-04-06 10:07:02 +02:00
DeadManWalking
15adae088c Translated using Weblate (Greek)
Currently translated at 27.2% (200 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
2022-04-06 10:07:02 +02:00
Arsenicus
0e3ca147a2 Translated using Weblate (Russian)
Currently translated at 95.5% (702 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
2022-04-06 10:07:02 +02:00
Artur Adamczyk
89ca64a7a0 Translated using Weblate (Polish)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:02 +02:00
Jakub Z
7f71996e02 Translated using Weblate (Polish)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:02 +02:00
ROllerozxa
530b5a1c00 Translated using Weblate (Swedish)
Currently translated at 70.4% (518 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
2022-04-06 10:07:02 +02:00
Andrij Mizyk
2e2bf46553 Translated using Weblate (Ukrainian)
Currently translated at 31.1% (229 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
2022-04-06 10:07:02 +02:00
Andrij Mizyk
195f5c12c4 Translated using Weblate (Ukrainian)
Currently translated at 8.9% (66 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
2022-04-06 10:07:02 +02:00
Jakub Z
f6be8e3546 Translated using Weblate (Polish)
Currently translated at 92.6% (681 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:02 +02:00
ROllerozxa
c452c5b528 Translated using Weblate (Swedish)
Currently translated at 64.3% (473 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
2022-04-06 10:07:02 +02:00
THANOS SIOURDAKIS
924cdc5d49 Translated using Weblate (Greek)
Currently translated at 25.7% (189 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/el/
2022-04-06 10:07:01 +02:00
Jakub Z
c77bceefa1 Translated using Weblate (Polish)
Currently translated at 89.7% (660 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:01 +02:00
ROllerozxa
ab5c2bf384 Translated using Weblate (Swedish)
Currently translated at 40.6% (299 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
2022-04-06 10:07:01 +02:00
THANOS SIOURDAKIS
28b08a7138 Added translation using Weblate (Greek) 2022-04-06 10:07:01 +02:00
Jakub Z
601a38aec2 Translated using Weblate (Polish)
Currently translated at 59.4% (437 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:01 +02:00
Jun Nogata
2e5bf618dc Translated using Weblate (Japanese)
Currently translated at 22.5% (166 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
2022-04-06 10:07:01 +02:00
Jakub Z
c0fbf806de Translated using Weblate (Polish)
Currently translated at 52.3% (385 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/pl/
2022-04-06 10:07:01 +02:00
ROllerozxa
be73d1b48f Translated using Weblate (Swedish)
Currently translated at 22.5% (166 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/sv/
2022-04-06 10:07:01 +02:00
Jakub Z
6e98b55afb Added translation using Weblate (Polish) 2022-04-06 10:07:01 +02:00
ROllerozxa
d180e05117 Added translation using Weblate (Swedish) 2022-04-06 10:07:01 +02:00
MinecraftTAO
625e16d215 Translated using Weblate (Chinese (Simplified))
Currently translated at 56.1% (413 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
2022-04-06 10:07:01 +02:00
Minetest-j45
d2deb46110 Translated using Weblate (Spanish)
Currently translated at 67.0% (493 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
2022-04-06 10:07:01 +02:00
Gao Tiesuan
75b8d191ff Translated using Weblate (Chinese (Simplified))
Currently translated at 54.4% (400 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
2022-04-06 10:07:01 +02:00
debiankaios
f87c292b74 Translated using Weblate (German)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-04-06 10:07:01 +02:00
Jun Nogata
a01cc55591 Translated using Weblate (Japanese)
Currently translated at 9.9% (73 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
2022-04-06 10:07:01 +02:00
debiankaios
f32ba909b7 Translated using Weblate (German)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-04-06 10:07:01 +02:00
neinwhal
936852cafb Translated using Weblate (Chinese (Simplified))
Currently translated at 54.5% (401 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
2022-04-06 10:07:01 +02:00
Nikita Epifanov
d6005f9543 Translated using Weblate (Russian)
Currently translated at 95.3% (701 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
2022-04-06 10:07:01 +02:00
Maxime Leroy
5418abd820 Translated using Weblate (French)
Currently translated at 98.7% (726 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
2022-04-06 10:07:01 +02:00
neinwhal
90bff5fd0b Translated using Weblate (Chinese (Simplified))
Currently translated at 40.2% (296 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
2022-04-06 10:07:01 +02:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
b68c9ff64f Translated using Weblate (Malay)
Currently translated at 100.0% (735 of 735 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
2022-04-06 10:07:01 +02:00
Y.W
9b9234929b Translated using Weblate (Chinese (Simplified))
Currently translated at 31.1% (229 of 735 strings)

Co-authored-by: Y.W <y5nw@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-04-06 10:07:01 +02:00
neinwhal
6d9f2e8b8c Translated using Weblate (Chinese (Simplified))
Currently translated at 31.1% (229 of 735 strings)

Co-authored-by: neinwhal <fishyWET@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-04-06 10:07:01 +02:00
rubenwardy
2a1672544f Disable game support in UI for now 2022-04-02 20:12:49 +01:00
rubenwardy
8f622ba5c9 Add ability to search for text in all packages 2022-04-02 20:09:59 +01:00
rubenwardy
e42f6b2cfa Add 500 page 2022-02-15 15:26:03 +00:00
rubenwardy
9200d7becd Add missing package attribution to remove() 2022-02-14 01:21:24 +00:00
rubenwardy
a70454cf1f Extend number of audit log entries shown on /todo/ 2022-02-14 01:19:32 +00:00
rubenwardy
07db1943fb Add audit log to editor todo 2022-02-14 01:18:03 +00:00
rubenwardy
3d35f6507a Add temp banning and ban messages 2022-02-13 10:41:02 +00:00
rubenwardy
7a650eb1e4 Fix unfulfilled dependency checker 2022-02-13 09:53:12 +00:00
rubenwardy
d7c765c972 Disable game support updating 2022-02-13 09:13:53 +00:00
rubenwardy
173261a69f Emails: Fix crash due to missing connection 2022-02-13 09:03:50 +00:00
rubenwardy
5c5608680b Add validation for modpack names with one mod 2022-02-12 18:25:46 +00:00
rubenwardy
09eea443cf Update translations 2022-02-12 14:58:49 +00:00
Wuzzy
d471720541 Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Co-authored-by: Wuzzy <almikes@aol.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-02-12 15:57:06 +01:00
Maxime Leroy
17270000eb Translated using Weblate (French)
Currently translated at 98.7% (718 of 727 strings)

Co-authored-by: Maxime Leroy <lisacintosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-02-12 15:57:06 +01:00
AFCMS
154cc97603 Translated using Weblate (French)
Currently translated at 98.7% (718 of 727 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-02-12 15:57:06 +01:00
gemmaro
3b6f243940 Translated using Weblate (Japanese)
Currently translated at 6.7% (49 of 727 strings)

Added translation using Weblate (Japanese)

Co-authored-by: gemmaro <gemmaro.dev@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ja/
Translation: Minetest/ContentDB
2022-02-12 15:57:06 +01:00
Maxime Leroy
0a149ed440 Translated using Weblate (French)
Currently translated at 97.2% (707 of 727 strings)

Co-authored-by: Maxime Leroy <lisacintosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-02-12 15:57:06 +01:00
rubenwardy
595b86df6c Add Precedence: Bulk header to bulk emails 2022-02-12 14:13:57 +00:00
rubenwardy
770d17b42a Use persistent SMTP connection for bulk emails, add List-Unsubscribe header 2022-02-12 14:06:04 +00:00
rubenwardy
8ad066409c Fix notification digest issue 2022-02-11 17:17:11 +00:00
rubenwardy
4ac8949c3a Disable Celery concurrency, to see if it fixes the session issue 2022-02-10 19:24:37 +00:00
rubenwardy
83b2cf48d4 Fix audit crash 2022-02-10 02:20:12 +00:00
rubenwardy
2bbb117eac Small fixes 2022-02-09 19:14:08 +00:00
rubenwardy
f61112a8d7 Add ability for moderators to convert reviews into threads 2022-02-09 12:47:36 +00:00
rubenwardy
3566b030c5 Attempt to fix package session issues 2022-02-08 10:40:20 +00:00
rubenwardy
2d54fe4ed7 Fix issues with Package sets by adding a PackageSet class 2022-02-07 18:10:43 +00:00
rubenwardy
7fdd2cc7c9 Fix small typos in dev_intro 2022-02-06 23:08:58 +00:00
rubenwardy
81a85cbbe5 Update dev intro 2022-02-06 22:58:13 +00:00
rubenwardy
4902436b6b Add start of developer's intro 2022-02-06 22:07:22 +00:00
rubenwardy
b82bcb0af9 Disable 'Submit for Approval' when release is broken 2022-02-04 13:36:26 +00:00
rubenwardy
eeea5d004a Fix "specific" typo 2022-02-03 17:04:09 +00:00
rubenwardy
97ee0a9f85 Fix crash on game support update 2022-02-03 17:02:01 +00:00
rubenwardy
958f92fd63 Add single API to upload cover image 2022-02-02 01:29:14 +00:00
rubenwardy
dfef268b05 Fix docs on cover-image 2022-02-02 01:21:33 +00:00
rubenwardy
e7d2f09eb4 Add cover_image to screenshot response in API 2022-02-02 01:20:11 +00:00
rubenwardy
5bb9012655 Add game support to API 2022-02-02 01:11:44 +00:00
rubenwardy
a291b2cd6f Add cover_image API
Fixes #360
2022-02-02 01:08:01 +00:00
rubenwardy
ead077fb92 Metapackages: Split up "provided" into multiple subheadings 2022-02-02 00:56:56 +00:00
rubenwardy
1c9d6ac865 Rename "Supported Games" to "Compatible Games" 2022-02-02 00:29:16 +00:00
rubenwardy
d098ee9dff Run game support update_all on unapproved mods too 2022-02-02 00:25:59 +00:00
rubenwardy
b8d95dd222 Add disclaimer to Supported Games section on package pages 2022-02-01 21:54:11 +00:00
rubenwardy
7c93db95a3 Add community hub to list game content 2022-02-01 21:22:28 +00:00
rubenwardy
d529634b7f Add game support detection
Part of #232
2022-02-01 20:56:43 +00:00
Y.W
765b5603c1 Translated using Weblate (Chinese (Simplified))
Currently translated at 28.1% (205 of 727 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 28.1% (205 of 727 strings)

Co-authored-by: Y.W <y5nw@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
Gao Tiesuan
eec39a3fc5 Translated using Weblate (Chinese (Simplified))
Currently translated at 28.1% (205 of 727 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
72f66530aa Translated using Weblate (Malay)
Currently translated at 100.0% (727 of 727 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
2022-02-01 15:55:32 +01:00
Nikita Epifanov
99ee1cfc7e Translated using Weblate (Russian)
Currently translated at 95.4% (694 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
rubenwardy
f8e82b63e3 Revert "Limit visibility of unapproved packages to maintainers and approvers" and "Fix 404 on packages when not logged in"
This reverts commit 85a178d90e.
This reverts commit 727db52c19.
2022-02-01 14:54:09 +00:00
rubenwardy
afdf06b3f6 Remove confusing min/max version text 2022-01-30 19:27:21 +00:00
rubenwardy
d21a86587f Fix content_flags documentation 2022-01-30 19:24:33 +00:00
rubenwardy
38071165d1 Add welcome dialog API 2022-01-30 03:35:32 +00:00
rubenwardy
1cfc152d3b Fix crash on needs tags in user todo 2022-01-29 20:23:15 +00:00
rubenwardy
2db2f61992 Enable Russian language 2022-01-29 20:23:00 +00:00
Balázs Kovács
4543f6ca39 Translated using Weblate (Hungarian)
Currently translated at 18.9% (138 of 727 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
2022-01-29 20:51:41 +01:00
Nikita Epifanov
f8d518300d Translated using Weblate (Russian)
Currently translated at 95.4% (694 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Andrij Mizyk
347e214944 Translated using Weblate (Ukrainian)
Currently translated at 6.1% (45 of 727 strings)

Added translation using Weblate (Ukrainian)

Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Mikitko
99b4d8e084 Translated using Weblate (Russian)
Currently translated at 93.9% (683 of 727 strings)

Co-authored-by: Mikitko <rudzik8@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Nikita Epifanov
313cab6b2d Translated using Weblate (Russian)
Currently translated at 93.8% (682 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
debiankaios
494559cfd7 Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translated using Weblate (German)

Currently translated at 97.7% (711 of 727 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
e3326aa0f1 Translated using Weblate (Malay)
Currently translated at 100.0% (727 of 727 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
2022-01-29 20:51:41 +01:00
rubenwardy
bdd3ab4360 Add is_protected and views to Tags API 2022-01-29 19:26:55 +00:00
rubenwardy
4f9ec2e8a4 Fix attempting to set protected tag in API dropping other tags 2022-01-29 19:25:02 +00:00
rubenwardy
14fd30c4f4 Fix attempt to call module list 2022-01-27 19:20:45 +00:00
rubenwardy
a7103b5b35 Update Redis 2022-01-27 18:59:37 +00:00
rubenwardy
f6ce676e7e Add migration to fix fulltext search 2022-01-27 18:54:04 +00:00
rubenwardy
c2fbf7603a Update to Python 3.10 2022-01-27 18:44:00 +00:00
rubenwardy
c3a4ea239c Update dependencies 2022-01-27 18:21:47 +00:00
rubenwardy
e2708933d3 Clean up admin blueprint 2022-01-26 19:12:48 +00:00
rubenwardy
cb2d9d4b07 Add note about bug reports to report page 2022-01-26 18:16:47 +00:00
rubenwardy
1ba70226b8 Update translations 2022-01-26 03:09:18 +00:00
rubenwardy
d08710684d Add screenshot resolution checking 2022-01-26 03:08:00 +00:00
rubenwardy
625e4cf9ee Allow removing video_url 2022-01-25 23:32:51 +00:00
rubenwardy
c8b310ebdb Fix 2022-01-25 23:28:38 +00:00
rubenwardy
d971dd6700 Update translations 2022-01-25 22:37:51 +00:00
rubenwardy
e20863a7e1 Support links to video hosts other than YouTube 2022-01-25 22:14:06 +00:00
rubenwardy
8f2a87e5ed Harden video_embed.js, store URL in data-src 2022-01-25 21:52:46 +00:00
rubenwardy
ae88360e20 Fix unsubscribe crash 2022-01-25 21:38:02 +00:00
rubenwardy
7d97c2a27b Fix notification digest crash 2022-01-25 21:37:54 +00:00
rubenwardy
02b7d55c2d Add remind_video_url() admin action 2022-01-25 21:37:35 +00:00
rubenwardy
55b5893cce Update translations 2022-01-25 21:04:39 +00:00
rubenwardy
1018e1c29c Add support for YouTube video embeds
Fixes #75
2022-01-25 21:00:45 +00:00
rubenwardy
e5a4161e76 Fix crash due to typo whilst commiting 2022-01-25 18:09:32 +00:00
rubenwardy
a3f437e482 Redesign download button 2022-01-25 17:27:40 +00:00
rubenwardy
9fcbbdc472 Refactor get_locale() to be cleaner 2022-01-25 01:35:57 +00:00
rubenwardy
7aac597216 Change User.locale default to None 2022-01-25 01:33:13 +00:00
rubenwardy
95b3c66366 Copy locale to User model 2022-01-25 01:22:47 +00:00
rubenwardy
3b354de2fc Lower email sending rate limit again 2022-01-23 18:40:06 +00:00
rubenwardy
411392eb76 Lower email sending rate limit 2022-01-23 18:27:46 +00:00
rubenwardy
15c3e4edec Raise email sending rate limit 2022-01-23 18:25:58 +00:00
rubenwardy
fa0572ae44 Move preferred language/locale point in privacy policy 2022-01-23 18:10:25 +00:00
rubenwardy
ade75ace49 Update privacy policy
- Add preferred language
- Add admin bulk email sending
- Update location
2022-01-23 17:56:54 +00:00
Hugo Locurcio
56539bb369 Fix missing space before "and" in package list (#357) 2022-01-23 17:21:29 +00:00
Y.W
1c63bf0beb Translated using Weblate (Chinese (Simplified))
Currently translated at 21.4% (152 of 708 strings)

Co-authored-by: Y.W <y5nw@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-01-23 18:15:15 +01:00
pampogo kiraly
b10949d8cd Translated using Weblate (Hungarian)
Currently translated at 15.9% (113 of 708 strings)

Co-authored-by: pampogo kiraly <pampogo.kiraly@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2022-01-23 18:15:15 +01:00
debiankaios
853cc3ff6e Translated using Weblate (German)
Currently translated at 100.0% (708 of 708 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-23 18:15:15 +01:00
rubenwardy
a0cc6eb997 Translated using Weblate (Spanish)
Currently translated at 64.4% (456 of 708 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-23 18:15:15 +01:00
J. Lavoie
8b18e6f86d Translated using Weblate (French)
Currently translated at 89.2% (632 of 708 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-23 18:15:15 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
68e4d98bc5 Translated using Weblate (Malay)
Currently translated at 100.0% (708 of 708 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
2022-01-23 18:15:15 +01:00
rubenwardy
390bf7a657 Fix two crashes due to translations 2022-01-23 17:14:03 +00:00
rubenwardy
deb5c02ce6 Fix sending error email on email ratelimit 2022-01-22 22:11:36 +00:00
rubenwardy
004c5cd383 Allow translating emails
Fixes #350
2022-01-22 21:23:01 +00:00
rubenwardy
7b4254da58 Add locale to user model 2022-01-22 20:47:43 +00:00
rubenwardy
d4903f04f1 Update translations 2022-01-22 20:29:17 +00:00
debiankaios
f2b544ae68 Translated using Weblate (German)
Currently translated at 100.0% (697 of 697 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-22 21:28:02 +01:00
Lemente
ec91295677 Translated using Weblate (French)
Currently translated at 89.8% (626 of 697 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-22 21:28:02 +01:00
rubenwardy
4943fbd776 Translated using Weblate (Spanish)
Currently translated at 64.5% (450 of 697 strings)

Translated using Weblate (French)

Currently translated at 89.8% (626 of 697 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-22 21:28:01 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
2478df8c0d Translated using Weblate (Malay)
Currently translated at 100.0% (697 of 697 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
2022-01-22 21:28:01 +01:00
rubenwardy
85a178d90e Fix 404 on packages when not logged in 2022-01-22 00:04:09 +00:00
rubenwardy
a48c0fb2b4 Fix unapproved content showing in "related"
Fixes #253
2022-01-21 22:49:12 +00:00
rubenwardy
3c944cbd72 Add moderator tools page
Fixes #341
2022-01-21 22:33:22 +00:00
rubenwardy
727db52c19 Limit visibility of unapproved packages to maintainers and approvers
Fixes #338
2022-01-21 21:48:15 +00:00
rubenwardy
80d534a53f Fix crash on invalid username in forums import 2022-01-21 21:20:04 +00:00
rubenwardy
fe2d08c395 Allow moderators to edit ThreadReplies 2022-01-21 14:30:12 +00:00
rubenwardy
97e2e1c16e Add abs_url_samesite 2022-01-21 14:23:27 +00:00
rubenwardy
a32b63f932 Use relative URLs in report, to ensure correct links 2022-01-21 14:17:50 +00:00
rubenwardy
e0421c1e57 Disable WIP markdown editor toolbar option 2022-01-21 13:59:13 +00:00
rubenwardy
f457f7f5d7 Fix accidental new line in thread <title> 2022-01-21 13:15:07 +00:00
rubenwardy
3ac2d937d7 Fix grammatically weird <title> on thread pages 2022-01-21 13:14:02 +00:00
rubenwardy
45eca10859 Fix "staff" typo 2022-01-21 02:53:46 +00:00
rubenwardy
38aa8fa03a Translated using Weblate (Malay)
Currently translated at 98.1% (684 of 697 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2022-01-21 03:52:57 +01:00
rubenwardy
11036b113b Update translations 2022-01-21 01:25:01 +00:00
Wuzzy
f5893676eb Translated using Weblate (German)
Currently translated at 100.0% (686 of 686 strings)

Co-authored-by: Wuzzy <almikes@aol.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-21 02:23:50 +01:00
Lemente
d7b5b1eedb Translated using Weblate (French)
Currently translated at 90.6% (622 of 686 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-21 02:23:50 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
e44ec8720d Translated using Weblate (Malay)
Currently translated at 100.0% (686 of 686 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
2022-01-21 02:23:49 +01:00
Gao Tiesuan
6b592053f1 Translated using Weblate (Chinese (Simplified))
Currently translated at 13.7% (94 of 686 strings)

Added translation using Weblate (Chinese (Simplified))

Added translation using Weblate (Chinese (Literary))

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-01-21 02:23:49 +01:00
rubenwardy
ef28fa026e Fix crash due to misnamed path argument in abs_url_for 2022-01-20 23:40:47 +00:00
rubenwardy
e1a86f3be0 Remove report link on help pages 2022-01-20 23:37:17 +00:00
rubenwardy
7f5656df08 Add reporting system
Fixes #12
2022-01-20 23:30:56 +00:00
rubenwardy
a47e6e8998 Move /email_sent/ to flask endpoint, to allow translation 2022-01-20 21:55:16 +00:00
rubenwardy
b6fe0466ca Increase locale cookie expiry to 5 years 2022-01-20 20:27:38 +00:00
rubenwardy
9ea4ee3449 Fix crash due to mistake in tags.html 2022-01-20 14:49:46 +00:00
rubenwardy
d9a6127c35 Fix inconsistent capitalisation of GitHub and Javascript
Fixes #353
2022-01-20 01:29:02 +00:00
rubenwardy
3ad003140f Fix inconsistent naming of reviews vs approvals
Fixes #354
2022-01-20 01:18:13 +00:00
Wuzzy
d7152485bb Translated using Weblate (German)
Currently translated at 100.0% (684 of 684 strings)

Co-authored-by: Wuzzy <almikes@aol.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-20 01:48:59 +01:00
AFCMS
0f17dbc15d Translated using Weblate (French)
Currently translated at 85.9% (588 of 684 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-19 08:54:16 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
e1cc4bbdf0 Translated using Weblate (Malay)
Currently translated at 100.0% (684 of 684 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
2022-01-19 08:54:15 +01:00
rubenwardy
a325d2c2cd Fix crash due to missing import in screenshots.py 2022-01-17 22:32:09 +00:00
rubenwardy
da1ae4c270 Upgrade PostgreSQL to v14 2022-01-17 18:15:47 +00:00
rubenwardy
9cc79d9fa5 Fix mention linkifying emails 2022-01-17 18:15:23 +00:00
rubenwardy
a09f11d110 Show profile picture on package page
Fixes #327
2022-01-17 18:15:08 +00:00
rubenwardy
6e93e6d777 Fix optional_depends not being validated 2022-01-17 15:34:41 +00:00
Joaquín Villalba
b05bd78e20 Translated using Weblate (Spanish)
Currently translated at 40.7% (279 of 684 strings)

Co-authored-by: Joaquín Villalba <joaco-mono@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
waxtatect
5a27e1a03b Translated using Weblate (French)
Currently translated at 85.3% (584 of 684 strings)

Co-authored-by: waxtatect <piero@live.ie>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
Lemente
0f3628f2a4 Translated using Weblate (French)
Currently translated at 85.3% (584 of 684 strings)

Co-authored-by: Lemente <crafted.by.lemente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
Mehmet Ali
b3fcf4d1c2 Translated using Weblate (Turkish)
Currently translated at 3.3% (23 of 684 strings)

Added translation using Weblate (Turkish)

Co-authored-by: Mehmet Ali <2045uuttb@relay.firefox.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/tr/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
debiankaios
01a9afdd9d Translated using Weblate (German)
Currently translated at 100.0% (684 of 684 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
Yiu Man Ho
3ad1ebdb7b Translated using Weblate (Chinese (Traditional))
Currently translated at 7.8% (54 of 684 strings)

Added translation using Weblate (Chinese (Traditional))

Co-authored-by: Yiu Man Ho <yiufamily.hh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hant/
Translation: Minetest/ContentDB
2022-01-17 16:22:48 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
903d567e3c Translated using Weblate (Malay)
Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Malay)

Currently translated at 95.4% (653 of 684 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
2022-01-17 16:22:48 +01:00
rubenwardy
6a4bf7129d Add user and package mentions 2022-01-17 15:06:03 +00:00
rubenwardy
e02c014890 Fix format instead of variable in gettext 2022-01-14 18:28:22 +00:00
rubenwardy
beb916d521 Fix some untranslatable text 2022-01-14 18:25:33 +00:00
rubenwardy
f3856b5db5 Improve donation panel on package pages 2022-01-14 17:52:33 +00:00
rubenwardy
8af2942097 Enable German translation 2022-01-14 15:28:46 +00:00
pampogo kiraly
dcfdf299e3 Translated using Weblate (Hungarian)
Currently translated at 17.5% (115 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
2022-01-13 23:35:33 +01:00
debiankaios
ca139bab54 Translated using Weblate (German)
Currently translated at 100.0% (656 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-13 23:35:32 +01:00
pampogo kiraly
80b63d3d24 Added translation using Weblate (Hungarian) 2022-01-13 21:25:46 +01:00
cx384
c550f2395f Translated using Weblate (German)
Currently translated at 99.8% (655 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-13 18:43:58 +01:00
debiankaios
f7040ecc8f Translated using Weblate (German)
Currently translated at 99.8% (655 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-13 18:43:54 +01:00
debiankaios
8bd0fe0662 Translated using Weblate (German)
Currently translated at 91.9% (603 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-13 18:31:11 +01:00
cx384
e5cb738252 Translated using Weblate (German)
Currently translated at 91.9% (603 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-13 18:31:08 +01:00
debiankaios
c016060553 Translated using Weblate (German)
Currently translated at 81.5% (535 of 656 strings)

Translated using Weblate (German)

Currently translated at 69.3% (455 of 656 strings)

Translated using Weblate (German)

Currently translated at 68.1% (447 of 656 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-13 18:05:16 +01:00
cx384
9e59be7d65 Translated using Weblate (German)
Currently translated at 81.5% (535 of 656 strings)

Translated using Weblate (German)

Currently translated at 69.3% (455 of 656 strings)

Translated using Weblate (German)

Currently translated at 68.1% (447 of 656 strings)

Translated using Weblate (German)

Currently translated at 65.0% (427 of 656 strings)

Co-authored-by: cx384 <muelladresse84@web.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-13 18:05:16 +01:00
Joaquín Villalba
5e2fc9155c Translated using Weblate (Spanish)
Currently translated at 34.7% (228 of 656 strings)

Co-authored-by: Joaquín Villalba <joaco-mono@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-13 18:05:15 +01:00
rubenwardy
543499560d Fix crash in package_approval.html translation 2022-01-13 03:09:22 +00:00
cx384
cff7964831 Translated using Weblate (German)
Currently translated at 59.2% (389 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-12 21:50:47 +01:00
Muhammad Rifqi Priyo Susanto
9ad8a7f420 Translated using Weblate (Indonesian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
2022-01-12 21:50:47 +01:00
Minetest-j45
2fab6cd6ae Translated using Weblate (Spanish)
Currently translated at 29.1% (191 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
2022-01-12 21:50:47 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
ef192dcaee Translated using Weblate (Malay)
Currently translated at 100.0% (656 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
2022-01-12 21:50:47 +01:00
Buckaroo Banzai
9d817c71e3 Translated using Weblate (German)
Currently translated at 56.0% (368 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-12 21:50:47 +01:00
debiankaios
0bee59d7c3 Translated using Weblate (German)
Currently translated at 56.0% (368 of 656 strings)

Translation: Minetest/ContentDB
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
2022-01-12 21:50:46 +01:00
activivan
76cd2a6786 Translated using Weblate (German)
Currently translated at 52.7% (346 of 656 strings)

Co-authored-by: activivan <activivan@mail.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
Buckaroo Banzai
04bba2e135 Translated using Weblate (German)
Currently translated at 52.7% (346 of 656 strings)

Co-authored-by: Buckaroo Banzai <postmaster@rudin.io>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
Linerly
dab25f6789 Translated using Weblate (Indonesian)
Currently translated at 100.0% (656 of 656 strings)

Co-authored-by: Linerly <linerly@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
Joaquín Villalba
c725451206 Translated using Weblate (Spanish)
Currently translated at 26.6% (175 of 656 strings)

Co-authored-by: Joaquín Villalba <joaco-mono@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
Imre Kristoffer Eilertsen
617c7900ff Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.8% (19 of 656 strings)

Co-authored-by: Imre Kristoffer Eilertsen <imreeil42@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nb_NO/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
waxtatect
e8dea0d69d Translated using Weblate (French)
Currently translated at 68.2% (448 of 656 strings)

Co-authored-by: waxtatect <piero@live.ie>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
AFCMS
0fe71ec86f Translated using Weblate (French)
Currently translated at 68.2% (448 of 656 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
Mikitko
5ac69c5051 Translated using Weblate (Russian)
Currently translated at 28.9% (190 of 656 strings)

Added translation using Weblate (Russian)

Co-authored-by: Mikitko <rudzik8@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
debiankaios
0518aa8650 Translated using Weblate (German)
Currently translated at 40.2% (264 of 656 strings)

Added translation using Weblate (German)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-12 21:50:46 +01:00
rubenwardy
7ffecbb318 Fix key package API returning display_name rather than username 2022-01-12 20:50:11 +00:00
rubenwardy
e0a92c6455 Add /api/dependencies/ 2022-01-12 17:08:18 +00:00
rubenwardy
3af5fccd61 Fix crash due to missing request.endpoint 2022-01-12 16:47:39 +00:00
rubenwardy
fbadb05037 Fix crash due to missing view_args 2022-01-11 17:06:24 +00:00
rubenwardy
416daa868b Add hint to reason in package removal 2022-01-09 21:14:59 +00:00
rubenwardy
34ccd76b0c Add reason to package removal 2022-01-09 21:12:29 +00:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
db14b3f4ef Translated using Weblate (Malay)
Currently translated at 100.0% (656 of 656 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
2022-01-09 02:01:55 +01:00
rubenwardy
ca0823c460 Fix optional dependencies being presolved 2022-01-08 22:29:02 +00:00
rubenwardy
33d9ab4b86 Fix duplicated messages in translation template 2022-01-08 20:15:10 +00:00
rubenwardy
ceed91b6d7 Translated using Weblate (Indonesian)
Currently translated at 99.0% (651 of 657 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-01-08 20:11:15 +00:00
Minetest-j45
aa8409b0be Translated using Weblate (Spanish)
Currently translated at 13.3% (88 of 657 strings)

Co-authored-by: Minetest-j45 <janscheresmonesma@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-08 20:11:07 +00:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
5f45c31240 Translated using Weblate (Malay)
Currently translated at 85.0% (559 of 657 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
2022-01-08 20:10:52 +00:00
rubenwardy
71b4a0416f Fix remaining translation parameters 2022-01-08 17:57:05 +00:00
rubenwardy
00bb8a486d Fix translation parameters 2022-01-08 16:57:43 +00:00
rubenwardy
3a0a3c5325 Enable Indonesian 2022-01-08 16:40:51 +00:00
Allan Nordhøy
1e839f731a Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.7% (18 of 657 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nb_NO/
Translation: Minetest/ContentDB
2022-01-08 17:39:21 +01:00
AFCMS
97ae05b864 Translated using Weblate (French)
Currently translated at 49.3% (324 of 657 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-08 17:39:20 +01:00
rubenwardy
48a8a45140 Translated using Weblate (Malay)
Currently translated at 66.3% (436 of 657 strings)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2022-01-08 17:39:20 +01:00
Muhammad Rifqi Priyo Susanto
88da170bb0 Translated using Weblate (Indonesian)
Currently translated at 98.9% (650 of 657 strings)

Translated using Weblate (Indonesian)

Currently translated at 57.2% (376 of 657 strings)

Added translation using Weblate (Indonesian)

Co-authored-by: Muhammad Rifqi Priyo Susanto <muhammadrifqipriyosusanto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/id/
Translation: Minetest/ContentDB
2022-01-08 17:39:19 +01:00
rubenwardy
ec6f16c229 Fix crash when sending emails 2022-01-08 02:42:48 +00:00
rubenwardy
db4e3dabb7 Fix crash due to misspelled gettext arg 2022-01-08 02:35:17 +00:00
rubenwardy
b2a72da219 Enable Melay translation 2022-01-08 02:34:19 +00:00
rubenwardy
cf0a69a702 Update translation templates 2022-01-08 02:20:16 +00:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
572d6bd9ea Translated using Weblate (Malay)
Currently translated at 68.5% (435 of 635 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
2022-01-08 03:19:22 +01:00
rubenwardy
574339f935 Fix crash in template 2022-01-08 01:49:13 +00:00
rubenwardy
baa8c871b0 Fix crash on unimported gettext 2022-01-08 01:47:25 +00:00
rubenwardy
b62bdb016a Fix placeholders in translations 2022-01-07 23:31:32 +00:00
rubenwardy
63c6ccfee9 Update translation templates 2022-01-07 23:27:54 +00:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
db24385f40 Translated using Weblate (Malay)
Currently translated at 11.3% (52 of 457 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
2022-01-08 00:27:29 +01:00
rubenwardy
c5a6ae3035 Allow translating text in templates 2022-01-07 23:27:00 +00:00
rubenwardy
c8b0f9e6ce Allow translating text in blueprints 2022-01-07 22:11:12 +00:00
rubenwardy
bd59fa8ef3 Update translations 2022-01-07 21:59:18 +00:00
AFCMS
503ae701ae Translated using Weblate (French)
Currently translated at 80.8% (275 of 340 strings)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-07 22:58:51 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi
a7089b26e7 Translated using Weblate (Malay)
Currently translated at 5.5% (19 of 340 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
2022-01-07 22:58:51 +01:00
rubenwardy
1b26acaaae Allow translating form labels 2022-01-07 21:55:33 +00:00
rubenwardy
dcd7e31738 Allow translating flash messages 2022-01-07 21:46:16 +00:00
rubenwardy
c4dd380218 Allow translating package form 2022-01-07 21:18:34 +00:00
rubenwardy
ad05ba1ee8 Fix search bad text bug 2022-01-07 21:15:48 +00:00
rubenwardy
a175162186 Remove flask-menu, make navbar translatable 2022-01-07 21:09:09 +00:00
rubenwardy
b40bc8c20d Tag index.html and base.html, test updating translation templates 2022-01-07 20:58:32 +00:00
rubenwardy
44b02cfb4e Add translation tagging call for help 2022-01-07 20:27:49 +00:00
rubenwardy
9de4ad5cb3 Fix crash 2022-01-07 19:46:49 +00:00
rubenwardy
482c9e5905 Enable translation support 2022-01-07 19:42:52 +00:00
Joaquín Villalba
eba1626f2e Translated using Weblate (Spanish)
Currently translated at 1.2% (4 of 321 strings)

Added translation using Weblate (Spanish)

Co-authored-by: Joaquín Villalba <joaco-mono@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/es/
Translation: Minetest/ContentDB
2022-01-06 23:17:40 +01:00
J. Lavoie
f5f6671d48 Translated using Weblate (French)
Currently translated at 72.8% (234 of 321 strings)

Translated using Weblate (French)

Currently translated at 69.4% (223 of 321 strings)

Translated using Weblate (French)

Currently translated at 64.4% (207 of 321 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-06 23:17:39 +01:00
Allan Nordhøy
868bbed290 Translated using Weblate (Norwegian Bokmål)
Currently translated at 4.6% (15 of 321 strings)

Added translation using Weblate (Norwegian Bokmål)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/nb_NO/
Translation: Minetest/ContentDB
2022-01-06 23:17:39 +01:00
AFCMS
10846d481c Translated using Weblate (French)
Currently translated at 79.4% (255 of 321 strings)

Translated using Weblate (French)

Currently translated at 69.4% (223 of 321 strings)

Translated using Weblate (French)

Currently translated at 64.4% (207 of 321 strings)

Translated using Weblate (French)

Currently translated at 28.3% (91 of 321 strings)

Added translation using Weblate (French)

Co-authored-by: AFCMS <afcm.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/fr/
Translation: Minetest/ContentDB
2022-01-06 23:17:38 +01:00
rubenwardy
757c1f8c45 Translated using Weblate (Malay)
Currently translated at 0.0% (0 of 321 strings)

Translated using Weblate (Malay)

Currently translated at 0.0% (0 of 321 strings)

Added translation using Weblate (Malay)

Co-authored-by: rubenwardy <rw@rubenwardy.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2022-01-06 23:17:38 +01:00
rubenwardy
d8f164ffc1 Fix thread typo when sending webhook 2022-01-05 00:27:48 +00:00
rubenwardy
324cbe9efc Add translation template 2022-01-04 19:08:03 +00:00
rubenwardy
e4ea44aa5b Allow null dev_state 2022-01-04 13:08:53 +00:00
rubenwardy
122e1a4677 Rename replies to comments 2022-01-03 01:44:12 +00:00
rubenwardy
933d8ebfe7 Add all user replies page 2022-01-03 01:41:50 +00:00
rubenwardy
8f4e214c52 Add review votes page 2022-01-01 22:40:27 +00:00
rubenwardy
e346587111 Remove thumbs from helpful votes 2022-01-01 21:45:03 +00:00
rubenwardy
4dfb35a57b Remove gamejam banner 2022-01-01 02:27:49 +00:00
rubenwardy
d16666c0f8 Update homepage gamejam notice 2021-12-23 12:07:38 +00:00
rubenwardy
4d37f53a04 Fix crash due to null dev_state 2021-12-23 11:58:39 +00:00
rubenwardy
e3ed5fbc58 Show WIP packages in client, add missing keys to package 2021-12-23 11:56:03 +00:00
rubenwardy
2e7d4277e1 Allow editors to restore packages 2021-12-20 23:56:44 +00:00
rubenwardy
5932ac3c7c Clean up hide query argument in QueryBuilder 2021-12-20 21:24:06 +00:00
rubenwardy
5d32d7922f Add Maintenance State field
Fixes #160
2021-12-20 21:07:12 +00:00
rubenwardy
a800685947 Improve webhook error on create events 2021-12-12 17:20:23 +00:00
rubenwardy
7aca5a54dc Add more to reviews API response 2021-11-26 14:56:01 +00:00
rubenwardy
e1cd2ceb1d Improve reviews API docs 2021-11-26 14:42:21 +00:00
rubenwardy
c46cca519a Fix paginated API response key 2021-11-26 14:34:37 +00:00
rubenwardy
da41fb5738 Improve reviews API 2021-11-26 14:33:17 +00:00
rubenwardy
bd25a8d601 Fix limit arg in QueryBuilder 2021-11-25 15:51:41 +00:00
rubenwardy
c13b13268b Update topic queries API doc 2021-11-25 15:48:23 +00:00
rubenwardy
10cfbc6e45 Fix typo in URL in API docs 2021-11-25 11:13:58 +00:00
rubenwardy
6c99732673 Fix UI test assertion 2021-11-25 11:01:07 +00:00
rubenwardy
3c4085eb0b Use username in fallback when generating gravatar profile picture 2021-11-25 11:00:20 +00:00
rubenwardy
443dd9f18f Fix user count assertion in UI tests 2021-11-25 10:56:45 +00:00
rubenwardy
95c0fb8a70 Fix migration 2021-11-25 10:51:54 +00:00
rubenwardy
a04b2542b5 Add policy on Featured Packages (#323) 2021-11-25 10:49:42 +00:00
rubenwardy
49355f5db1 Improve API documentation 2021-11-25 10:49:31 +00:00
rubenwardy
41e0e65a6b Add more info to /api/homepage/ 2021-11-25 10:35:31 +00:00
rubenwardy
f714d809f8 Fix crash on /api/homepage/ due to invalid response returned 2021-11-25 10:28:15 +00:00
rubenwardy
cd39f7b2c6 Fix verification email not being sent by set password 2021-11-25 01:49:14 +00:00
rubenwardy
c4c8390ead Fix typo in API docs 2021-11-24 23:16:05 +00:00
dependabot[bot]
02311f190b Bump babel from 2.9.0 to 2.9.1 (#340)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 23:14:58 +00:00
rubenwardy
085c99272e Show gamejam banner to all users 2021-11-24 23:14:32 +00:00
rubenwardy
5fd1666a5d Prevent metrics roll over 2021-11-24 23:01:42 +00:00
rubenwardy
c0eb10521d Delete unconfirmed accounts after 12 hours 2021-11-24 22:54:35 +00:00
rubenwardy
bc371f1ef3 Delete inactive user accounts after 12 hours 2021-11-24 17:58:03 +00:00
rubenwardy
0486eb76c0 Add 12 hour expiry to email verification tokens 2021-11-24 17:41:39 +00:00
rubenwardy
3b5c9950de Add created_at to User and UserEmailVerification 2021-11-24 17:35:38 +00:00
rubenwardy
dd352faa31 Improve email verification help 2021-11-24 17:10:45 +00:00
rubenwardy
4f69dd8d32 Add API for reviews 2021-11-24 16:33:37 +00:00
rubenwardy
d0741fde6e Also allow Authorization header 2021-11-24 15:49:00 +00:00
rubenwardy
d485e686d9 Allow cross-origin requests to the API 2021-11-24 15:39:50 +00:00
rubenwardy
ae37a551e1 Improve gamejam image cropping 2021-11-23 01:16:59 +00:00
rubenwardy
afb2f9ec00 Add gamejam redirect 2021-11-23 01:05:43 +00:00
rubenwardy
21d5d9d47e Add gamejam.png 2021-11-23 00:59:43 +00:00
rubenwardy
20c93925a8 Add game jam ad 2021-11-23 00:58:53 +00:00
rubenwardy
e5ae41901c Add rules for requesting reviews 2021-11-21 03:44:44 +00:00
dependabot[bot]
d8b68136ef Bump pillow from 8.2.0 to 8.3.2 (#336)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 15:32:47 +01:00
rubenwardy
5319ea8771 Fix package form type/title/name field sizes 2021-09-08 16:54:06 +01:00
rubenwardy
43fcf5ee3b Add hint to readonly package name 2021-09-08 16:51:37 +01:00
rubenwardy
46a38753a9 Fix review votes not cascade deleting 2021-08-21 22:55:11 +01:00
rubenwardy
32372e8e86 Add support for strikethrough in markdown
Fixes #304
2021-08-21 05:40:20 +01:00
rubenwardy
c1edea4dc3 Allow removing review votes
Fixes #328
2021-08-19 13:45:32 +01:00
rubenwardy
86e1f57198 Hide review vote counts when 0 2021-08-18 22:49:44 +01:00
rubenwardy
fab814c46f Add review voting 2021-08-18 22:09:41 +01:00
rubenwardy
4a1f654798 Fix crash due to faulty game detection 2021-08-18 02:14:15 +01:00
rubenwardy
895a113478 Fallback to autogenerated Gravatars 2021-08-17 21:34:23 +01:00
rubenwardy
37a7dd28d6 Add optional Discord webhook support 2021-08-17 21:16:43 +01:00
rubenwardy
e5cc140d42 Add Approver rank 2021-08-16 18:57:05 +01:00
rubenwardy
59a5cf2df5 Add editor queue for packages with "Other" licenses
Fixes #283
2021-08-12 18:57:25 +01:00
rubenwardy
d6b1adf613 Disable package name validation on modpacks
Whilst modpacks providing an incorrect name in `modpack.conf` is wrong,
it doesn't actually matter as ContentDB will correct it after installation.
2021-08-08 20:43:57 +01:00
rubenwardy
562b0ceffe Allow admin to delete any user (except admins) 2021-08-04 21:50:35 +01:00
rubenwardy
6bbe2307e9 Add email notification prompt to post-login hook 2021-07-31 21:35:07 +01:00
rubenwardy
aae546a08e Import licenses from SPDX
Fixes #326
2021-07-31 21:03:45 +01:00
rubenwardy
2f2141f524 Allow editors to change maintainers
Fixes #325
2021-07-31 19:52:36 +01:00
rubenwardy
aee59626ee Add outdated packages notification 2021-07-30 19:50:52 +01:00
rubenwardy
825801b867 Refactor admin page 2021-07-30 19:43:02 +01:00
rubenwardy
447f3e2d5b Add modname uniqueness page 2021-07-29 19:34:47 +01:00
rubenwardy
ff846f4478 Fix bug with Editor screenshots 2021-07-28 01:53:44 +01:00
rubenwardy
c794de680b Profile medals: top package type icons, equal height 2021-07-26 00:02:55 +01:00
rubenwardy
034e5382ec Profile medals: refactor code 2021-07-25 23:41:34 +01:00
rubenwardy
e06ac1689c Profile medals: add progressbars, make top packages per type 2021-07-25 22:44:13 +01:00
rubenwardy
4de802c68d Add total downloads number on profiles with <50k 2021-07-25 20:26:01 +01:00
rubenwardy
33aedb233d Add maintained packages to user profile 2021-07-25 18:42:11 +01:00
rubenwardy
95bd1a50d9 Fix crash on user on None min_package_rank 2021-07-25 18:30:10 +01:00
rubenwardy
76675ad76b Add top packages badge to profile 2021-07-25 18:17:59 +01:00
rubenwardy
ac9b2207bf Fix profile badge icon use 2021-07-25 17:10:57 +01:00
rubenwardy
d7c83f58b9 Fix crash on profile with no pacakges 2021-07-25 17:01:21 +01:00
rubenwardy
d17bd5580e Fix badge hint 2021-07-25 16:36:39 +01:00
rubenwardy
94568c851a Add package downloads badge to profile page 2021-07-25 16:35:08 +01:00
rubenwardy
29bfc91683 Add top reviewer badge to profile page 2021-07-25 16:09:21 +01:00
rubenwardy
cb5fa4d6e7 Improve related packages on package page
Fixes #254
2021-07-25 03:54:08 +01:00
rubenwardy
fc7739be2c Add package name validation to postReleaseCheckUpdate
Fixes #186
2021-07-25 00:09:19 +01:00
rubenwardy
5a12b9e6c4 Fix crash in releases list due to missing current_user argument 2021-07-24 16:47:41 +01:00
rubenwardy
4e83adc032 Refactor package URL generation 2021-07-24 04:30:14 +01:00
rubenwardy
187202d363 Improve unified navigation further 2021-07-24 03:56:43 +01:00
rubenwardy
545968a71f Remove updateFromRelease feature 2021-07-24 03:09:33 +01:00
rubenwardy
f5aee035b3 Use vertical nav 2021-07-24 03:09:11 +01:00
rubenwardy
1389cf450c Unify package edit UI 2021-07-24 02:55:55 +01:00
rubenwardy
24e3b1505b Fix UI tests 2021-07-24 02:32:28 +01:00
rubenwardy
347f8e5a22 Add support for renaming users and package alias redirects
Fixes #270
2021-07-24 02:30:43 +01:00
rubenwardy
0614e6b28b Fix broken background for code blocks
Fixes #256
2021-07-24 01:14:28 +01:00
rubenwardy
823c06d3ea Prevent API from changing protected tags
Fixes #322
2021-07-24 00:43:55 +01:00
Warr1024
3049d17f5e Improve placeholder.png image
Closes #293
2021-07-22 18:46:14 +01:00
rubenwardy
e7818d7fb4 Tweak Featured styling, fix various issues 2021-07-22 18:33:24 +01:00
rubenwardy
7db6c6bba4 Fix missing state filter 2021-07-22 14:05:59 +01:00
rubenwardy
b87401a0c8 Fix missing mapPackages 2021-07-22 14:04:08 +01:00
rubenwardy
13b6ab04bb Bump CSS version 2021-07-22 13:52:59 +01:00
rubenwardy
148ece162c Add Featured packages and protected tags 2021-07-22 13:44:20 +01:00
rubenwardy
ce2bb3abad Fix Create Package not appearing for unprivileged users 2021-07-22 11:58:00 +01:00
rubenwardy
cfddf0ada3 Fix missing if-statement on screenshot approval badge 2021-07-22 11:54:18 +01:00
rubenwardy
54304cf3e0 Add Create Package button to profile page 2021-07-22 11:50:39 +01:00
rubenwardy
f1597622ea Add approval state labels to screenshots and package tiles 2021-07-22 11:43:43 +01:00
rubenwardy
8c44b08682 Remove flower 2021-07-21 03:50:43 +01:00
rubenwardy
6fa6203ce0 😢 2021-07-21 02:31:51 +01:00
rubenwardy
75c118c483 Prefetches dependencies of likely dependency candidates 2021-07-20 21:24:08 +01:00
rubenwardy
4238dbd412 Allow maintainers to delete their bot threads 2021-07-20 00:25:55 +01:00
rubenwardy
9a54ada0ec Don't mark package as changes needed when creating a review thread 2021-07-19 23:59:40 +01:00
rubenwardy
ce8ae30311 Add editor notification when user replies to ContentDB system user 2021-07-19 23:55:00 +01:00
rubenwardy
2f77a84ec5 Add notification when release creation fails 2021-07-19 23:49:29 +01:00
rubenwardy
f83605c35f Fix too long Draft/ChangesNeeded messages 2021-07-19 23:11:02 +01:00
rubenwardy
4c4bddeed6 Add post_login function, go to notifications page 2021-07-19 23:04:55 +01:00
rubenwardy
4523849641 Add Draft/ChangesNeeded notification admin action 2021-07-19 23:04:55 +01:00
dependabot[bot]
f49c60d7f6 Bump urllib3 from 1.26.4 to 1.26.5 (#303)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-18 18:05:16 +01:00
dependabot[bot]
2452fceeda Bump pillow from 8.1.1 to 8.2.0 (#305)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-18 18:05:04 +01:00
rubenwardy
fb2f71e1dc Revert "Remove error on game missing hard deps"
This reverts commit aff20f1a6d.
2021-07-18 17:59:35 +01:00
rubenwardy
52437d4e2e Fix images exceeding long desc bounds
Fixes #309
2021-07-18 16:49:28 +01:00
rubenwardy
72a95ecfca Fix error with submodule cloning
Call `git submodule update --init` manually rather than using GitPython's
submodule API, as that fails with the following repo as a submodule:
https://files.creativekara.fr/git/naturalslopeslib.git
2021-07-18 02:48:19 +01:00
Jordan Irwin
9e95b69c11 Fix badges not displaying when dash (-) in title (#308) 2021-06-23 23:57:26 +01:00
rubenwardy
231c2a3a1e Fix broken audit log in thread log 2021-06-14 17:40:38 +01:00
rubenwardy
7dbea9f042 Fix crash on packages with no releases 2021-05-08 00:30:57 +01:00
rubenwardy
9dfb95a524 Use secrets library to generate tokens 2021-05-06 14:51:22 +01:00
rubenwardy
e9161610c4 Fix bad download file names
Fixes #116
2021-05-04 22:06:31 +01:00
rubenwardy
f4792ac537 Fix get_latest_tag() crash on repos with dangling tags
Fixes #272
2021-05-04 01:36:54 +01:00
rubenwardy
588b03cf34 Add tests for login and register 2021-05-03 23:57:22 +01:00
rubenwardy
94bf83c611 Add tests for git utils 2021-05-03 22:22:17 +01:00
rubenwardy
4bb35953b1 Test dependencies API 2021-05-03 18:47:19 +01:00
rubenwardy
c6f3f61ff6 Fix build status badge 2021-05-03 18:43:11 +01:00
rubenwardy
d64463235c Add tests for package release filtering 2021-05-03 18:27:32 +01:00
rubenwardy
dcc34570d5 Use GitHub actions (#295) 2021-05-03 17:59:23 +01:00
rubenwardy
464c85295a Update FUNDING.yml 2021-05-03 17:28:57 +01:00
rubenwardy
95bdababb3 Create FUNDING.yml 2021-05-03 17:27:56 +01:00
rubenwardy
a33a4bd894 Fix editors not being able to approve releases 2021-04-28 23:18:16 +01:00
rubenwardy
c0719fdeaa Add simple captcha 2021-04-17 02:12:00 +01:00
dependabot[bot]
3612c1747e Bump lxml from 4.6.2 to 4.6.3
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.2 to 4.6.3.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.2...lxml-4.6.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-10 16:33:35 +01:00
dependabot[bot]
a30b1bbf71 Bump pillow from 8.1.0 to 8.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.0...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-10 16:33:28 +01:00
dependabot[bot]
a0ace027d3 Bump urllib3 from 1.26.3 to 1.26.4
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-10 16:33:19 +01:00
rubenwardy
8dbd22f56c Add custom 404 page 2021-04-10 16:30:19 +01:00
rubenwardy
c2994a27fd Fix maintainers not being able to delete releases 2021-03-07 15:00:36 +00:00
rubenwardy
9cb9f8a4f6 Hotfix: Prevent webhooks from running on non-master/main branches 2021-03-07 14:48:04 +00:00
rubenwardy
4d2833de88 Add all releases API 2021-03-05 12:55:21 +00:00
rubenwardy
adcbf7455e Fix incorrect status code for Found 2021-03-02 22:37:37 +00:00
rubenwardy
df8ef542dd Disallow spaces in usernames 2021-03-02 22:36:21 +00:00
rubenwardy
c11e5c1f99 Revert "Fix Git clone error when checking out reference"
This reverts commit 63cfb5eac0.
2021-03-02 16:42:38 +00:00
rubenwardy
63cfb5eac0 Fix Git clone error when checking out reference 2021-03-01 18:10:46 +00:00
rubenwardy
6861524641 Improve release ratelimit message, increase limit 2021-03-01 18:10:33 +00:00
rubenwardy
032d8bf67b Fix minmum length on packages titles
Fixes #263
2021-02-28 21:32:28 +00:00
rubenwardy
47797f1fb1 Make it more clear that .conf will override release min/max vers 2021-02-28 21:30:54 +00:00
rubenwardy
92764465e0 Disable code highlighting for non-annotated code fences
Fixes #279
2021-02-28 21:24:57 +00:00
rubenwardy
04e108c31e Fix API crash due to missing upper() in enum coercion 2021-02-28 16:04:40 +00:00
rubenwardy
aead579f0b Use pen icon instead of edit 2021-02-28 05:19:57 +00:00
rubenwardy
f9089319d3 Improve package page sidebar 2021-02-28 05:19:57 +00:00
rubenwardy
ff2f7caee1 Add support for README files when importing meta
Fixes #87
2021-02-28 02:36:31 +00:00
rubenwardy
da81df535a API Screenshots: Fix crash on not a number 2021-02-28 01:14:43 +00:00
rubenwardy
7078ed3ac3 Fix duplicate name checking in register form 2021-02-27 19:11:53 +00:00
rubenwardy
da6b4b210f Allow specifying display name on register 2021-02-27 19:03:52 +00:00
rubenwardy
04f659bc2b API: Add support for specifying commit for zip releases 2021-02-27 18:31:56 +00:00
rubenwardy
8ef74deec1 Fix duplicate check for changing display names 2021-02-25 23:31:29 +00:00
rubenwardy
7e20a09499 Increase severity of display name change audit log 2021-02-25 23:29:27 +00:00
rubenwardy
dea5a52c86 Allow users to change their display name
Fixes #269
2021-02-25 23:25:33 +00:00
rubenwardy
96b5b4ea5b Add link to monitor and API 2021-02-25 23:03:32 +00:00
rubenwardy
aec346e2d4 Fix names not being included in topic query 2021-02-25 14:11:10 +00:00
rubenwardy
b41e4b50d9 Fix comment box padding 2021-02-23 00:46:16 +00:00
rubenwardy
3c095544d0 Enable word break in markdown content 2021-02-23 00:08:36 +00:00
rubenwardy
77dcb85912 Add username validation to signup page 2021-02-22 23:45:20 +00:00
rubenwardy
3ed73c4145 Fix crash on login with redirect 2021-02-11 23:26:23 +00:00
rubenwardy
c37f589765 Add package audit page 2021-02-08 00:47:34 +00:00
rubenwardy
7ff92bc7c1 Add ability to filter by no tags on package tags page 2021-02-05 17:03:50 +00:00
rubenwardy
3839dfbf90 Fix tag selector dropdown style 2021-02-05 16:42:48 +00:00
rubenwardy
0ff4f40652 Fix screenshot reordering 2021-02-05 16:05:20 +00:00
rubenwardy
2797792322 Add search box and edit tags button to Package Tags page 2021-02-05 16:01:12 +00:00
rubenwardy
3ce653ba74 Update documentation 2021-02-05 15:44:00 +00:00
rubenwardy
0918b8b676 Update front-end dependencies 2021-02-05 14:21:34 +00:00
rubenwardy
63204575eb Prevent duplicate commit releases from webhooks 2021-02-04 19:54:26 +00:00
rubenwardy
fb3b0be50e Update minetest_client.md docs 2021-02-03 18:30:21 +00:00
rubenwardy
0c08738a66 Update API docs 2021-02-03 12:50:16 +00:00
rubenwardy
21cf5b57c1 Redesign sign in screen 2021-02-03 00:58:49 +00:00
rubenwardy
b5f47b1b73 Fix typo 2021-02-03 00:14:14 +00:00
rubenwardy
05c140da78 Update docs, rename desc to long_description 2021-02-03 00:11:48 +00:00
rubenwardy
8225e4098b Add support for .cdb.json
Fixes #231
2021-02-02 23:58:59 +00:00
rubenwardy
90aeb6e1a7 Add missing fields to packages API 2021-02-02 23:30:11 +00:00
rubenwardy
12e364969b Add licenses API 2021-02-02 22:41:48 +00:00
rubenwardy
ca58c70206 Add validation to package API 2021-02-02 22:34:51 +00:00
rubenwardy
551996ca14 Add start of package edit API 2021-02-02 21:35:29 +00:00
rubenwardy
bb79d564a8 Fix missing bcrypt dep 2021-02-02 21:07:15 +00:00
rubenwardy
878872799e Fix crash on wtfpl page 2021-02-02 20:43:10 +00:00
rubenwardy
aa7b8a0fc0 Update dependencies 2021-02-02 20:18:53 +00:00
rubenwardy
14810b2cc5 Add table of contents to help pages 2021-02-02 20:05:24 +00:00
rubenwardy
5017a9ba7e Allow codehighlighting in markdown, enable linkify 2021-02-02 18:16:57 +00:00
rubenwardy
a040c7dd2e Clean up audit_log reasons 2021-02-02 17:29:03 +00:00
rubenwardy
912ebbc409 Add API to delete releases 2021-02-02 17:09:28 +00:00
rubenwardy
e1fe63ab19 Add more screenshot APIs 2021-02-02 17:09:25 +00:00
rubenwardy
509f03ce65 Add API to create screenshots 2021-02-02 17:09:23 +00:00
rubenwardy
64a897b52f Improve token form 2021-02-02 17:09:21 +00:00
rubenwardy
2f66db5989 Update API docs, add support for code highlighting, add markdown table support 2021-02-02 17:09:19 +00:00
rubenwardy
033f40c263 Add support for zip uploads in the API
Fixes #261
2021-02-02 00:19:32 +00:00
rubenwardy
a78fe8ceb9 Split importtasks.py 2021-02-01 22:43:43 +00:00
rubenwardy
c6a973f7e1 Add ability to filter by minetest-mods in outdated page 2021-02-01 21:18:27 +00:00
rubenwardy
d7647520c8 Fix erroneous updates from Git Update Detector 2021-02-01 11:34:55 +00:00
rubenwardy
70f491fd27 Add Editor help page 2021-01-31 17:51:25 +00:00
rubenwardy
f07f2803f8 Fix crash in get_latest_tag due to missing tuple 2021-01-31 11:24:04 +00:00
rubenwardy
4364ce5d6f Fix incorrect query in check_update_config task 2021-01-30 23:51:09 +00:00
rubenwardy
7c3d738756 Fix index out of range error in get_latest_tag 2021-01-30 23:42:00 +00:00
rubenwardy
ede010c25d Fix condition in apply_all_updates 2021-01-30 22:59:55 +00:00
rubenwardy
db09b8eb84 Add ability to bulk create releases for outdated packages 2021-01-30 22:58:55 +00:00
rubenwardy
a0cd155730 Add ability to bulk set update config 2021-01-30 22:44:54 +00:00
rubenwardy
b7814d9541 Check for existing releases with commit in update checker 2021-01-30 19:56:27 +00:00
rubenwardy
912b917a47 Fix API packages with no arguments 2021-01-30 19:49:15 +00:00
rubenwardy
c0112828eb Optimise package query speed 2021-01-30 19:32:04 +00:00
rubenwardy
b3237b0c49 Split utils.py into package 2021-01-30 18:25:00 +00:00
rubenwardy
b22ef5ae83 Add release creation audit logs 2021-01-30 17:10:38 +00:00
rubenwardy
8d6661511a Update copyrights 2021-01-30 17:00:58 +00:00
rubenwardy
607c534174 Improve outdated messages: don't show last commit on tag triggers 2021-01-30 16:50:32 +00:00
rubenwardy
3b213889ca Improve outdated messages 2021-01-30 16:48:11 +00:00
rubenwardy
36dc51ef4a Fix broken query condition in user todo 2021-01-30 16:07:39 +00:00
rubenwardy
663cbd91f5 Add missing tags section to user todo list 2021-01-30 16:03:09 +00:00
rubenwardy
82fe0e7bbf Add All Package Tags to todo tabs 2021-01-30 15:53:13 +00:00
rubenwardy
324815d58d Default to sort=score on All Outdated Packages 2021-01-30 15:45:48 +00:00
rubenwardy
a67e3af172 Improve update config form 2021-01-30 15:41:55 +00:00
rubenwardy
0cd23f7883 Add hints to update config form 2021-01-30 15:22:19 +00:00
rubenwardy
1b296fcae5 Update documentation 2021-01-30 15:18:56 +00:00
rubenwardy
84d7030f7d Consistently use "Git Update Detection" 2021-01-30 15:09:31 +00:00
rubenwardy
2fddc276de Add note to outdated packages in user todo 2021-01-30 15:05:22 +00:00
rubenwardy
a92ef0a8a1 Fix crash due to wrong method call to get screenshot thumbnail 2021-01-30 14:57:38 +00:00
rubenwardy
99eee9c758 Use utc date format when creating releases using Update Config 2021-01-30 01:21:26 +00:00
rubenwardy
56ff354021 Fix new release not reseting outdated flag 2021-01-30 01:19:43 +00:00
rubenwardy
ac4d5c8c88 Add page with list of all update configs 2021-01-30 01:08:00 +00:00
rubenwardy
c5fa76dab0 Add permissions check in outdated macro 2021-01-30 00:50:50 +00:00
rubenwardy
33bf3304a1 Add help message and button to outdated packages in to do list 2021-01-30 00:43:21 +00:00
rubenwardy
53d2d18b89 Fix create release title in warning on package page 2021-01-30 00:25:48 +00:00
rubenwardy
fa23a00014 Fix create release link in warning on package page 2021-01-30 00:24:38 +00:00
rubenwardy
81b24c6cb3 Add outdated warning to package page 2021-01-30 00:15:07 +00:00
rubenwardy
60a33a6492 Add ability to sort outdated packages list 2021-01-30 00:02:40 +00:00
rubenwardy
9acb7698ef Fix updates beat 2021-01-29 23:35:23 +00:00
rubenwardy
9e6ded6544 Fix empty message on user to do page 2021-01-29 23:30:30 +00:00
rubenwardy
ff5f98558d Fix notifications page package name width 2021-01-29 23:29:06 +00:00
rubenwardy
a088b1b0b5 Remove outdated packages section from editor work queue, as it's a tab now 2021-01-29 23:26:44 +00:00
rubenwardy
29adccb6d1 Add PackageUpdateConfig.auto_created 2021-01-29 23:18:37 +00:00
rubenwardy
c6d39fcba3 Add title and ref query args to create release 2021-01-29 23:12:26 +00:00
rubenwardy
fe2acddb5b Add datetime to outdated packages macro 2021-01-29 23:00:23 +00:00
rubenwardy
3dde8c05ad Improve wizard behaviour 2021-01-29 22:54:14 +00:00
rubenwardy
f49da74c3a Add help page for update detection 2021-01-29 22:50:18 +00:00
rubenwardy
53babc1113 Fix outdated packages page query 2021-01-29 22:37:17 +00:00
rubenwardy
09f8302e74 Add support for New Tag update detection trigger 2021-01-29 22:20:44 +00:00
rubenwardy
665bfd64d2 Bump CSS version 2021-01-29 20:27:33 +00:00
rubenwardy
cf5360f6f6 Don't disable update config on webhooks 2021-01-29 20:26:45 +00:00
rubenwardy
f1edfcebc0 Fix broken package icons 2021-01-29 20:21:17 +00:00
rubenwardy
ef9860b6cc Replace update bot message with notification only 2021-01-29 20:01:48 +00:00
rubenwardy
4f920f011f Add new to do list UI 2021-01-29 19:38:14 +00:00
rubenwardy
b613ac4b89 Fix default branch detection 2021-01-29 17:17:51 +00:00
rubenwardy
e8dca43f44 Remove webhook creation wizard 2021-01-29 16:36:23 +00:00
rubenwardy
46b60f9d24 Improve updateconfig docs further 2021-01-29 16:36:23 +00:00
rubenwardy
a02942b7e0 Disable updateconfig if webhooks are used 2021-01-29 16:36:23 +00:00
rubenwardy
693cf4250a Improve updateconfig docs 2021-01-29 16:36:23 +00:00
rubenwardy
ee9f6454e0 Change bot thread title 2021-01-29 16:36:23 +00:00
rubenwardy
c5d99e00d8 Document that new tag isn't implemented yet 2021-01-29 16:36:23 +00:00
rubenwardy
7be0616d38 Improve bot profile picture contrast 2021-01-29 16:36:23 +00:00
rubenwardy
ea527f9598 Only run update configs on approved packages 2021-01-29 16:36:23 +00:00
rubenwardy
8fcea988ca Add admin option to set updateconfigs on all relevant packages 2021-01-29 16:36:23 +00:00
rubenwardy
6b8b98c15b Add updateconfig error reporting 2021-01-29 16:36:23 +00:00
rubenwardy
17798df342 Add ref to update config 2021-01-29 16:36:23 +00:00
rubenwardy
2f2b8dc983 Add link to updateconfig from new release 2021-01-29 16:36:23 +00:00
rubenwardy
6e763b8453 Add notification type for bot messages 2021-01-29 16:36:23 +00:00
rubenwardy
09a9219fcd Add outdated flag to UpdateConfig to stop notification spam 2021-01-29 16:36:23 +00:00
rubenwardy
c8406b45d4 Add set up releases wizard 2021-01-29 16:36:23 +00:00
rubenwardy
14a67b99ba Add package update configuration for polling 2021-01-29 16:36:23 +00:00
rubenwardy
7461acdd1f Add link to Installing X on the package page 2021-01-29 16:27:38 +00:00
rubenwardy
88a8e85b12 Fix crash on forum profile not found 2021-01-27 17:42:47 +00:00
rubenwardy
5a1656b8d0 Add more information to minetest_client.md 2021-01-26 17:35:06 +00:00
rubenwardy
8fad3a15cd Improve documentation 2021-01-26 17:04:34 +00:00
rubenwardy
ce4c2142e2 Rename "Work in Progress" state to "Draft" 2021-01-25 16:58:58 +00:00
rubenwardy
6f9c01c375 Add pending verification note to email settings 2021-01-24 13:25:17 +00:00
rubenwardy
5e255a07f6 Add link to issue tracker in review form 2021-01-24 13:10:20 +00:00
rubenwardy
5314fda342 Fix broken user delete 2021-01-24 13:01:24 +00:00
rubenwardy
dfc6f6fd6e Fix crash on importing texture pack with no .conf 2021-01-21 20:47:44 +00:00
rubenwardy
05a08b4c05 Check for release key in minetestcheck 2021-01-16 18:19:51 +00:00
rubenwardy
07d7282383 Add APIs for tags and homepage 2021-01-16 00:34:09 +00:00
rubenwardy
01bed3e307 Add user-agnostic redirect to todo tags 2021-01-10 03:09:02 +00:00
dependabot[bot]
aabf70d458 Bump lxml from 4.5.2 to 4.6.2
Bumps [lxml](https://github.com/lxml/lxml) from 4.5.2 to 4.6.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.5.2...lxml-4.6.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-10 03:07:50 +00:00
rubenwardy
6d2558a921 Update top packages page 2021-01-10 03:03:07 +00:00
rubenwardy
28995ffdd6 Update policy and guidance 2021-01-10 03:00:28 +00:00
rubenwardy
e4d0b57f3c Improve Minetest config parsing error messages 2021-01-10 02:59:57 +00:00
rubenwardy
0054f362a7 Add account form to account settings page 2021-01-01 17:02:08 +00:00
rubenwardy
12bcdf2d47 Prevent moderators and admins from being deleted 2021-01-01 16:53:14 +00:00
rubenwardy
e709fc9ce3 Update new thread message 2021-01-01 16:36:39 +00:00
Lars Mueller
e0b490fdc0 Add support for multiline values in .conf files 2021-01-01 16:24:49 +00:00
rubenwardy
7964f5979a Add missing 'ago' to datetimes 2020-12-31 18:21:34 +00:00
rubenwardy
6ebab36877 Fix postReleaseCheckUpdate running twice on release creation 2020-12-31 18:19:07 +00:00
rubenwardy
afb699f8d3 Include mod name as prefix to zips created by git-archive-all 2020-12-29 20:42:43 +00:00
rubenwardy
d772f157eb Improve user dropdown 2020-12-22 16:09:28 +00:00
rubenwardy
1b81ff4d3b Update privacy policy 2020-12-22 13:14:49 +00:00
rubenwardy
8c5d997c6e Improve threads list design further 2020-12-22 12:54:48 +00:00
rubenwardy
c065519cca Add threads to navbar 2020-12-22 12:50:59 +00:00
rubenwardy
df79159e2e Improve thread list appearance 2020-12-22 12:39:32 +00:00
rubenwardy
1064885a2c Improve footer 2020-12-22 11:14:16 +00:00
rubenwardy
60362abef1 Improve claim user UX 2020-12-22 10:58:43 +00:00
rubenwardy
d7d9131de8 Add quote styling 2020-12-17 20:10:46 +00:00
rubenwardy
c44cc8082c Enable email notifications on claim login 2020-12-15 16:24:49 +00:00
rubenwardy
7a4335b8bc Improve form error messages 2020-12-15 12:56:17 +00:00
rubenwardy
8e3930d092 Fix failed login to unclaimed account 2020-12-15 12:29:30 +00:00
rubenwardy
5cbdaae5b3 Allow any maintainer/editor to set up GitHub webhooks 2020-12-14 21:22:11 +00:00
rubenwardy
c7aecd32be Fix error creating releases from certain git references
Fixes #249
2020-12-14 21:05:56 +00:00
rubenwardy
4820d11ce3 Tweak homepage row limits 2020-12-14 11:52:38 +00:00
rubenwardy
fc8cd3cfb8 Add top reviewed section to homepage 2020-12-14 11:48:26 +00:00
rubenwardy
fc9b8c2a5a Fix ungraceful crash when registering taken username 2020-12-13 14:01:18 +00:00
rubenwardy
9ec91fc52d Fix 'Remember Me' 2020-12-10 23:36:56 +00:00
rubenwardy
2ae4a2ed5a Fix broken includes in user models 2020-12-10 23:08:53 +00:00
rubenwardy
dfa5d0c5a7 Fix not being able to delete cover image 2020-12-10 23:07:13 +00:00
rubenwardy
fc3a481e6f Fix profile page crash when not logged in 2020-12-10 22:43:02 +00:00
rubenwardy
5ab8c2f0f1 Fix metapackages crash due to missing lazy=dynamic 2020-12-10 22:42:53 +00:00
rubenwardy
5a0aa636f3 Add ability to change cover image
Fixes #125
2020-12-10 22:40:20 +00:00
rubenwardy
fb1d33d27a Fix Package not using lazyloading for some relationships 2020-12-10 22:10:12 +00:00
rubenwardy
8d8577a941 Clean up database constraints 2020-12-10 22:10:12 +00:00
rubenwardy
70ac8fa6ab Convert models.py into package 2020-12-10 22:10:12 +00:00
rubenwardy
7088ffd321 Add ability for admin to hard delete packages 2020-12-10 22:10:12 +00:00
rubenwardy
e175e489e8 Use input-group for forum topic 2020-12-10 22:10:12 +00:00
rubenwardy
7efdf5cfef Fix minor things 2020-12-10 22:10:12 +00:00
rubenwardy
5fb01f01bf Fix broken audit links for normal users 2020-12-10 22:10:12 +00:00
rubenwardy
333dd60b32 Add logging of log ins 2020-12-10 22:10:12 +00:00
rubenwardy
4433c32afc Add ability to filter audit log by user 2020-12-10 22:10:12 +00:00
rubenwardy
d5190b0d76 Add audit log to account settings page 2020-12-10 22:10:12 +00:00
rubenwardy
58e1b924ca Add link to API docs in API Tokens page 2020-12-10 22:10:12 +00:00
rubenwardy
ac7714b997 Add account page to settings 2020-12-10 22:10:12 +00:00
rubenwardy
778a602aa6 Add user deletion / deactivation 2020-12-10 22:10:12 +00:00
rubenwardy
fd0b203f1e Add ability to delete threads
This reverts commit 78630b3071.
2020-12-10 22:10:12 +00:00
rubenwardy
b28732ee74 Use explicit back references in Database 2020-12-10 22:10:12 +00:00
rubenwardy
d8f33a4111 Add forum user redirect page 2020-12-07 20:11:40 +00:00
rubenwardy
396a620cf4 Fix visual glitch with discarded topics 2020-12-07 18:19:36 +00:00
rubenwardy
f7b3f4573d Add celery task maillogging 2020-12-07 18:17:17 +00:00
rubenwardy
9ead6c1481 Fix broken tag input due to jQuery UI update 2020-12-07 10:10:52 +00:00
rubenwardy
55dc6460d2 Change default notification settings 2020-12-06 15:04:09 +00:00
rubenwardy
3aa12be544 Add daily notification digests 2020-12-06 15:02:02 +00:00
rubenwardy
35e1659b77 Fix crash on missing GitLab field 2020-12-06 04:54:32 +00:00
rubenwardy
2a9e52d36b Add note about thumbnails to screenshot page 2020-12-06 04:48:40 +00:00
rubenwardy
3f48905331 Add screenshot placeholder on package page 2020-12-06 04:41:58 +00:00
rubenwardy
cf307e25d0 Add delete button to screenshot list 2020-12-06 04:30:47 +00:00
rubenwardy
4046c00a01 Improve edit/new screenshot appearance 2020-12-06 04:22:56 +00:00
rubenwardy
4226e945e6 Improve new screenshot behaviour 2020-12-06 04:08:31 +00:00
rubenwardy
f93a2d8717 Add ability to reorder screenshots 2020-12-06 03:37:05 +00:00
rubenwardy
2910fcc1a4 Improve notification description 2020-12-06 01:23:18 +00:00
rubenwardy
8ff61b4517 Fix incorrect sort order in notifications 2020-12-05 23:40:03 +00:00
rubenwardy
4944463f56 Partition Editor notifications away from normal notifications in the notifications list 2020-12-05 23:09:29 +00:00
rubenwardy
b3bd7ac615 Sort notifications in reverse order 2020-12-05 22:36:00 +00:00
rubenwardy
64a180ba8f Add reference validation
Fixes #158
2020-12-05 22:29:37 +00:00
rubenwardy
5a2ce15f96 Fix users being able to modify other user's email settings 2020-12-05 22:24:21 +00:00
rubenwardy
f6f4fe4fc6 Fix Email settings tab changing user 2020-12-05 22:20:43 +00:00
rubenwardy
a17260a4ee Add digest settings (despite not being implemented) 2020-12-05 22:03:05 +00:00
rubenwardy
4019e82f4a Add username-less redirect to email settings 2020-12-05 22:02:33 +00:00
rubenwardy
79230c1b0e Add bulk notification sending 2020-12-05 22:02:33 +00:00
rubenwardy
da3175e7bd Add email changed email 2020-12-05 22:02:33 +00:00
rubenwardy
d654113204 Remove email from user on unsubscribe 2020-12-05 22:02:33 +00:00
rubenwardy
6e3d32a9d5 Check for blacklisted emails in change email forms 2020-12-05 22:02:33 +00:00
rubenwardy
e1d6c4f5f5 Reorder 'Settings' in user dropdown 2020-12-05 22:02:33 +00:00
rubenwardy
085f0b49c6 Add unsubscribe 2020-12-05 22:02:33 +00:00
rubenwardy
5fe3b0b459 Add email send reasons 2020-12-05 22:02:33 +00:00
rubenwardy
3efda30b98 Add ability to send email in bulk 2020-12-05 22:02:33 +00:00
rubenwardy
683b855584 Enable email notifications for new users 2020-12-05 22:02:33 +00:00
rubenwardy
9c10e190bc Implement email notifications 2020-12-05 22:02:33 +00:00
rubenwardy
19308b645b Add privacy policy 2020-12-05 22:02:33 +00:00
rubenwardy
c46430c663 Add email to email tab, merge settings into settings.py file 2020-12-05 22:02:33 +00:00
rubenwardy
d976269f1a Add notice to notification settings 2020-12-05 22:02:33 +00:00
rubenwardy
c8e93a9f52 Add notification settings 2020-12-05 22:02:33 +00:00
rubenwardy
d32bb30071 Add notification types 2020-12-05 22:02:33 +00:00
rubenwardy
d5263acdf8 Fix switching between users in settings template 2020-12-05 03:16:09 +00:00
rubenwardy
8872ad33ad Tweak settings template 2020-12-05 03:12:33 +00:00
rubenwardy
7e29a621c3 Add missing login_required to profile edit 2020-12-05 02:42:44 +00:00
rubenwardy
dfb216a8df Log sensitive account changes 2020-12-05 02:42:32 +00:00
rubenwardy
f75bdec756 Add settings template 2020-12-05 02:20:21 +00:00
rubenwardy
0082870864 Add nav dropdown separators 2020-12-05 02:20:21 +00:00
rubenwardy
d0e1a95d9c Add missing migration 2020-12-05 02:20:21 +00:00
rubenwardy
f69fb47d69 Fix links missing icons in new profile 2020-12-05 02:20:21 +00:00
rubenwardy
4f52f82a15 Split profile into view and edit 2020-12-05 02:20:21 +00:00
rubenwardy
7c07ac22ad Add password suggestions to change and set password forms 2020-12-05 02:20:21 +00:00
rubenwardy
afb87c525d Improve verify email wording 2020-12-05 02:20:21 +00:00
rubenwardy
9b0ce41fd7 Fix signature parsing 2020-12-05 02:20:21 +00:00
rubenwardy
5f7c0a3b24 Implement password resets 2020-12-05 02:20:21 +00:00
rubenwardy
f7d90f2f53 Register: Fix behaviour on email conflict, add password suggestion 2020-12-05 02:20:21 +00:00
rubenwardy
43aab057c8 Implement change password 2020-12-05 02:20:21 +00:00
rubenwardy
bfcdd642fd Use NULL for non-existant passwords 2020-12-05 02:20:21 +00:00
rubenwardy
a8537659e2 Use correct mixin 2020-12-05 02:20:21 +00:00
rubenwardy
9620ceb842 Implement user registration and email confirmation 2020-12-05 02:20:21 +00:00
rubenwardy
5ef15e91d4 Remove flask_user and use flask_login directly, with partial feature support 2020-12-05 02:20:21 +00:00
rubenwardy
2358ed1b24 Remove WIP topic warning 2020-12-04 04:17:17 +00:00
rubenwardy
af8d8c330d Add created_at to approval queue list 2020-12-04 03:59:02 +00:00
rubenwardy
14f643592c Fix user conflict on forum import 2020-12-04 03:14:04 +00:00
rubenwardy
8c5cdb630e Fix forum parser 2020-12-04 02:57:36 +00:00
rubenwardy
b18903b59b Clean up JavaScript 2020-12-04 02:34:08 +00:00
rubenwardy
42f96618e2 Clean up code 2020-12-04 02:26:06 +00:00
rubenwardy
0c0d3e1715 Add setting SECRET_KEY to the Getting Started guide
Fixes #244
2020-12-04 02:00:05 +00:00
rubenwardy
2b06bca015 Set default git reference in create release form to None 2020-12-04 01:43:20 +00:00
rubenwardy
78630b3071 Revert "Add ability to delete threads"
This reverts commit 15821fe796.
2020-12-04 01:18:02 +00:00
rubenwardy
15063d92cd Require packages to have all hard deps in approval process 2020-12-04 01:00:57 +00:00
rubenwardy
15821fe796 Add ability to delete threads 2020-12-04 00:01:21 +00:00
rubenwardy
7d558ad7a2 Add pagination to audit log 2020-12-03 23:43:05 +00:00
rubenwardy
4242898e5d Add pagination to reviews 2020-12-03 23:41:11 +00:00
rubenwardy
d24f024cca Move screenshots to top of approval queue page 2020-12-03 23:36:24 +00:00
rubenwardy
ff93be7a89 Validate forum usernames in the claim form 2020-12-03 23:31:01 +00:00
rubenwardy
a47d222a47 Ignore IDEA files 2020-12-03 23:09:40 +00:00
rubenwardy
9f62c251f2 Fix error emails not preserving whitespace 2020-12-03 23:09:17 +00:00
rubenwardy
aff20f1a6d Remove error on game missing hard deps
Fixes #241
2020-12-03 21:08:20 +00:00
rubenwardy
6841a295ff Use contextlib to safely delete dirs in importtasks 2020-12-03 21:07:42 +00:00
rubenwardy
7a584e1a6e Fix failing UI test 2020-12-03 20:40:50 +00:00
rubenwardy
00be054135 Fix crash in GitLab webhook 2020-12-03 20:40:50 +00:00
dependabot[bot]
6eb4a803fd Bump cryptography from 2.9.2 to 3.2 (#242)
Bumps [cryptography](https://github.com/pyca/cryptography) from 2.9.2 to 3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/2.9.2...3.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-27 22:55:53 +00:00
rubenwardy
6503a82094 Fix crash on email templating 2020-10-19 15:30:45 +01:00
rubenwardy
31f52580c2 Change open package approval thread message 2020-09-23 19:17:06 +01:00
rubenwardy
2aa0c3cc84 Fix texture pack license not present issue
Fixes #236
2020-09-22 21:53:04 +01:00
rubenwardy
a3b3525b78 Add work queue icon to navigation bar 2020-09-19 19:30:33 +01:00
rubenwardy
d76f10c312 Improve documentation 2020-09-16 23:32:39 +01:00
rubenwardy
a1e0e37223 Fix broken state comparison due to enum ordering 2020-09-16 22:19:14 +01:00
rubenwardy
9a1c1c56e6 Allow admin to make a package WIP 2020-09-16 22:11:47 +01:00
rubenwardy
3a5fe25e12 Fix migration 2020-09-16 22:09:00 +01:00
rubenwardy
f56b6021d8 Fix crash on missing PackageState 2020-09-16 22:03:36 +01:00
rubenwardy
380c88b5a3 Improve release approval section appearance 2020-09-16 21:55:52 +01:00
rubenwardy
dd1288dc3c Sort notifications by date 2020-09-16 18:16:41 +01:00
rubenwardy
258a23cd9a Allow all users to delete their packages 2020-09-16 18:12:53 +01:00
rubenwardy
92fb54556a Implement package states for easier reviews 2020-09-16 17:51:03 +01:00
rubenwardy
e81eb9c8d5 Fix crash on no signature 2020-09-15 15:48:03 +01:00
rubenwardy
8ec4006cc7 Disable email in default config 2020-09-01 15:26:10 +01:00
rubenwardy
b3fdb991d6 Update README.md 2020-09-01 15:05:31 +01:00
rubenwardy
5b086bb559 Fix migration error when migrating from scratch 2020-09-01 14:57:03 +01:00
rubenwardy
934d581737 Fix screenshot form not validating length 2020-08-19 13:06:21 +01:00
rubenwardy
e85d1755f0 Increase thread/comment ratelimiting based on rank 2020-08-18 18:10:42 +01:00
rubenwardy
1c4fe1b80c Fix reimport not unapproving releases 2020-08-18 17:39:20 +01:00
rubenwardy
f6ff5cba82 Add unfulfilled dependencies todo page 2020-08-18 17:28:42 +01:00
rubenwardy
193e4e39b1 Split hard and soft dependers on meta package page 2020-08-18 17:13:37 +01:00
rubenwardy
ab7d5a3feb Show optional dependencies on games 2020-08-18 17:09:13 +01:00
rubenwardy
2279208b00 Check for game hard dependencies 2020-08-18 17:08:17 +01:00
rubenwardy
a8e1863341 Fix bug in meta package counting 2020-08-18 16:54:05 +01:00
rubenwardy
506974a50d Add forum topic list to meta packages page 2020-08-18 16:42:33 +01:00
rubenwardy
996ba82663 Add list of dependers to meta package page
Fixes #229
2020-08-18 16:29:51 +01:00
rubenwardy
68524adadf Remove provides/dependencies from Package form 2020-08-18 16:14:47 +01:00
rubenwardy
b8ee612b45 Update meta on release import 2020-08-18 16:12:27 +01:00
rubenwardy
5db633d911 Add ability to delete unused metapackages 2020-08-18 14:22:16 +01:00
rubenwardy
2f208d9239 Add regex checking for dependency names 2020-08-18 13:57:56 +01:00
rubenwardy
0c81d0ae2b Improve MinetestCheck name validation 2020-08-18 13:34:04 +01:00
rubenwardy
6167bdc7f0 Remove empty dependencies in MinetestCheck 2020-08-18 11:43:13 +01:00
rubenwardy
b50a306e66 Print updateMetaFromRelease info 2020-08-18 00:41:37 +01:00
rubenwardy
0b06cfffba Fix reimport packages not importing unapproved package 2020-08-18 00:28:27 +01:00
rubenwardy
85551539f0 Fix incorrect game names detected by MinetestCheck 2020-08-18 00:25:13 +01:00
rubenwardy
3914659718 Fix dependencies still being added if in provides
Fixes #226
2020-08-18 00:16:03 +01:00
rubenwardy
8fd229b739 Fix crash on null user agent 2020-08-16 13:13:25 +01:00
rubenwardy
d69da8e3ea Redirect to correct URL when _game is missing from package name 2020-08-02 18:03:44 +01:00
rubenwardy
9a64809542 Add badges/shields support 2020-08-02 17:41:06 +01:00
rubenwardy
ce034fddd4 Add downloads to package JSON 2020-08-02 17:10:47 +01:00
rubenwardy
e931d6a88b Fix comment length checking 2020-07-29 17:33:12 +01:00
rubenwardy
a8a3067ac9 Fix validation error in release form on incomplete URL 2020-07-18 15:42:49 +01:00
rubenwardy
64dab0c4b6 Filter tags by available packages in package search 2020-07-18 03:14:56 +01:00
rubenwardy
dd7146205a Add description title tooltips to tags 2020-07-18 02:54:40 +01:00
rubenwardy
68a132f271 Add tags list to homepage 2020-07-18 02:48:22 +01:00
rubenwardy
c7b1dcec4f Sort "recently added" by approved_at 2020-07-18 01:48:37 +01:00
rubenwardy
7d0a93483a Reorder homepage sections 2020-07-18 01:27:23 +01:00
rubenwardy
836caf0fe0 Add last updated section to homepage 2020-07-18 01:24:23 +01:00
rubenwardy
980e1c9eb1 Add ability to search admin tag list by views 2020-07-17 23:17:25 +01:00
rubenwardy
e2a9ea91cf Fix descriptions being required in warning and tag editors 2020-07-17 22:29:02 +01:00
rubenwardy
2a7318eca2 Add descriptions to tags, and show in multiselect 2020-07-17 22:08:34 +01:00
rubenwardy
b067fd2e77 Add support for filtering content warnings 2020-07-17 21:18:27 +01:00
rubenwardy
6a674c3c79 Add Content Warnings 2020-07-17 20:48:51 +01:00
rubenwardy
0ac2827468 Fix crash on bad wtforms validator instace 2020-07-17 20:07:51 +01:00
rubenwardy
054dfa4cbd Fix wrong character limit on review form 2020-07-16 18:41:27 +01:00
rubenwardy
74371d3fcb Check user-agent for crawlers before incrementing counters 2020-07-16 14:35:12 +01:00
rubenwardy
9d3ba8991d Add requirements lock file 2020-07-16 14:26:26 +01:00
rubenwardy
0e4722ea98 Add nofollow to tags 2020-07-16 14:07:21 +01:00
rubenwardy
208a47b41d Fix tag views redis cache 2020-07-16 13:52:18 +01:00
rubenwardy
7fb2f3170c Allow Editors to edit tags 2020-07-15 19:54:36 +01:00
rubenwardy
9663e87838 Count tag views 2020-07-15 19:06:00 +01:00
rubenwardy
8dd1cd9045 Increase comment length limit to 2000 2020-07-15 16:01:45 +01:00
rubenwardy
643380038b Fix broken ordering by reverting change to default 2020-07-15 15:44:59 +01:00
rubenwardy
27dfbabe2f Improve tags page layout and add link to profile 2020-07-15 00:54:26 +01:00
rubenwardy
15bbc35e65 Use query builder in tag list, add link to todo page 2020-07-15 00:21:20 +01:00
rubenwardy
c9e4638b34 Add start of bulk tag editor 2020-07-14 23:45:54 +01:00
rubenwardy
ff2cd6dc2f Fix dropdown menu alignment 2020-07-14 04:28:27 +01:00
rubenwardy
aa6892da82 Add admin function to import foreign release URLs 2020-07-14 00:28:56 +01:00
rubenwardy
3fbc5f7751 Filter out packages with no releases in ContentDB 2020-07-13 02:10:59 +01:00
rubenwardy
a57e06d09b Restrict seeing the email addresses of others to admins only 2020-07-13 00:34:05 +01:00
rubenwardy
bbc89bb2c2 Fix misattribution of review due to missing reply ordering 2020-07-12 23:53:20 +01:00
rubenwardy
ab58570a0c Redesign user list 2020-07-12 21:02:50 +01:00
rubenwardy
cd520a0251 Redesign version edit page 2020-07-12 20:36:32 +01:00
rubenwardy
8bcf12e1a7 Redesign tags and license edit pages 2020-07-12 20:34:16 +01:00
rubenwardy
ec087e4687 Move tag list to top of package list page 2020-07-12 20:19:00 +01:00
rubenwardy
ae4352068e Add tag filter list to package page 2020-07-12 20:10:19 +01:00
rubenwardy
2faa0e4219 Fix query sorting further 2020-07-12 17:56:06 +01:00
rubenwardy
2e3a9035c4 Fix pagination widget syntax error 2020-07-12 17:52:30 +01:00
rubenwardy
2e6f99d09e Fix fulltext search order being overriden 2020-07-12 17:52:15 +01:00
rubenwardy
f437850a50 Add global url_set_query Jinja template function 2020-07-12 17:15:30 +01:00
rubenwardy
820c968f73 Replace "Content DB" with "ContentDB" 2020-07-12 16:34:25 +01:00
rubenwardy
9d1f098d8a Fix clear notifications creating null user_ids 2020-07-12 16:33:17 +01:00
rubenwardy
d7ecf8041a Improve admin list design 2020-07-12 03:47:59 +01:00
rubenwardy
a123f42291 Fix incorrect mod name fold in MinetestCheck 2020-07-12 03:09:01 +01:00
rubenwardy
e6a7df6144 Fix migration misordering 2020-07-12 02:45:19 +01:00
rubenwardy
4bd9411d87 Add check constraint on MetaPackage name 2020-07-12 02:43:51 +01:00
rubenwardy
284683e7e5 Add reimport of package meta from latest release
Fixes #127
2020-07-12 02:22:35 +01:00
rubenwardy
868ced76a8 Fix bugs related to package owner not being a maintainer 2020-07-11 16:56:36 +01:00
rubenwardy
729241c0fe Remove full review form from package page 2020-07-11 04:41:47 +01:00
rubenwardy
8d48723158 Swap edit and delete buttons in comments 2020-07-11 04:29:59 +01:00
rubenwardy
2fb2f1ae49 Remove admin from being able to edit any comment 2020-07-11 04:26:50 +01:00
rubenwardy
d5b8dd8909 Add title text to audit log severity icons 2020-07-11 04:14:57 +01:00
rubenwardy
dfbcbbbb47 Add ability to edit comments 2020-07-11 03:53:03 +01:00
rubenwardy
08f6bd8bef Move DELETE_REPLY permission to ThreadReply 2020-07-11 03:35:14 +01:00
rubenwardy
31b8a7931b Add ability for moderators to delete comments 2020-07-11 03:29:38 +01:00
rubenwardy
a4dd4f0429 Add audit log 2020-07-11 02:32:17 +01:00
rubenwardy
bf927c50f0 Add the ability to lock threads 2020-07-11 01:42:47 +01:00
rubenwardy
5f7be4b433 Add package and created_at to Notifications 2020-07-11 00:53:03 +01:00
rubenwardy
9bf20df941 Fix typo in previous commit 2020-07-11 00:06:21 +01:00
rubenwardy
adc31962c0 Merge padlock symbol with icons in thread lists 2020-07-11 00:05:44 +01:00
rubenwardy
0e9b8a1a82 Fix title wrapping in /threads/ 2020-07-11 00:01:03 +01:00
rubenwardy
6150447c85 Fix bootstrap toggle button not matching backing radio button 2020-07-10 23:55:46 +01:00
rubenwardy
dd86fb0e14 Fix profile picture alignment in notifications page 2020-07-10 23:50:02 +01:00
rubenwardy
b483d5413f Add badge to notification icon 2020-07-10 23:46:36 +01:00
rubenwardy
c80ff2e709 Fix empty view in thread lists 2020-07-10 23:18:40 +01:00
rubenwardy
2181e57e42 Redesign notifications page 2020-07-10 23:12:15 +01:00
rubenwardy
c490df7f50 Add ability for moderators to change linked accounts 2020-07-10 22:59:41 +01:00
rubenwardy
b9e1be57e4 Fix generation of forum profile URLs
Fixes #196
2020-07-10 22:44:58 +01:00
rubenwardy
c3d96c7459 Add more sort options to package API, correct documentation
Fixes #204
2020-07-10 22:32:54 +01:00
rubenwardy
b9386d5a47 Use middleware to clear notifications
Fixes #70
2020-07-10 22:23:52 +01:00
rubenwardy
1d8abd8f4b Fix screenshot approval checkbox always being unchecked
Fixes #212
2020-07-10 22:19:47 +01:00
rubenwardy
0bf61dda08 Fix ellipsis in pagination 2020-07-10 22:11:34 +01:00
rubenwardy
660b813ff7 Fix pagination losing query arguments
Fixes #205
2020-07-10 22:08:52 +01:00
rubenwardy
ba3b108239 Fix tag selector losing all tags on remove
Fixes #148
2020-07-10 21:27:41 +01:00
rubenwardy
42b08f9bcd Fix tags being lost on Edit Package
Fixes #211
2020-07-10 21:02:40 +01:00
rubenwardy
849cdd257d Ignore FileExistsError in thumbnails 2020-07-10 20:50:25 +01:00
rubenwardy
16b174d882 Improve recommends styling on review edit form 2020-07-10 20:47:03 +01:00
rubenwardy
61e2c8a1c0 Remove accidental limit of 5 reviews on /reviews/ 2020-07-10 20:33:47 +01:00
rubenwardy
c7a7609763 Add /reviews/ to list all reviews 2020-07-10 20:31:29 +01:00
rubenwardy
13130a217c Use FontAwesome for navbar icons 2020-07-10 20:23:19 +01:00
rubenwardy
daa2d2989e Clean up view package button on reviews 2020-07-10 20:16:06 +01:00
rubenwardy
ee6de95a52 Allow editors to unapprove and delete packages 2020-07-10 20:13:48 +01:00
rubenwardy
1daf59b7db Improve thread list design further 2020-07-10 20:10:51 +01:00
rubenwardy
94e91e33b8 Fix view thread page title 2020-07-10 19:46:23 +01:00
rubenwardy
d91f537bdd Improve thread list style 2020-07-10 19:46:14 +01:00
rubenwardy
436a4cce2b Add ability to delete reviews 2020-07-10 19:26:37 +01:00
rubenwardy
71f9fe469a Change comments button color if there are comments 2020-07-10 19:14:23 +01:00
rubenwardy
76b0c8446c Hide review form on own package 2020-07-10 19:12:08 +01:00
rubenwardy
069c7de78c Add reviews to user profile 2020-07-10 19:10:36 +01:00
rubenwardy
3eeaf3be22 Hide reviews from package thread list 2020-07-10 19:06:27 +01:00
rubenwardy
1989eabf86 Add more obvious edit buttons for reviews 2020-07-10 19:01:58 +01:00
rubenwardy
491f9ed679 Fix GitHub claim method being broken by phpBB update 2020-07-10 18:41:08 +01:00
rubenwardy
000259fc88 Fix crash on sending notification 2020-07-09 05:54:39 +01:00
rubenwardy
078765fe44 Prevent users from reviewing their own packages 2020-07-09 05:47:26 +01:00
rubenwardy
45877bb3a4 Fix missing import 2020-07-09 05:45:46 +01:00
rubenwardy
eb3d067e26 Fix crash on addNotification non-iterable 2020-07-09 05:45:04 +01:00
rubenwardy
db80c441ec Fix crash when guests view package page 2020-07-09 05:34:55 +01:00
rubenwardy
849b814034 Fix margin above CDB stats on homepage 2020-07-09 05:34:25 +01:00
rubenwardy
37a4dbe66b Add distinction between review buttons 2020-07-09 05:31:41 +01:00
rubenwardy
75ab56cad1 Add recent positive reviews to homepage 2020-07-09 05:30:13 +01:00
rubenwardy
25b481ac0a Add package title and link to review page 2020-07-09 05:01:18 +01:00
rubenwardy
893507691b Show "Edit Review" button when a user already has a review 2020-07-09 04:50:49 +01:00
rubenwardy
ac7adde4b1 Add score bonus to reviews 2020-07-09 04:32:13 +01:00
rubenwardy
d0aecd0ee5 Rename triggerNotif to addNotification, add array support 2020-07-09 04:16:45 +01:00
rubenwardy
307b8f8dde Add reviews
Fixes #173
2020-07-09 04:10:09 +01:00
rubenwardy
9d033acfff Separate rolling average downloads from score 2020-07-09 01:26:01 +01:00
rubenwardy
2617c53abf Add downloads column to Package
Fixes #200
2020-07-09 01:11:50 +01:00
rubenwardy
bbf1143090 Fix incorrect link in maintainers list 2020-07-09 00:02:56 +01:00
rubenwardy
2a37608cb0 Remove package author from maintainers edit field 2020-07-08 23:58:53 +01:00
rubenwardy
3dd5e7445e Fix check when showing remove myself from maintainers 2020-07-08 23:44:13 +01:00
rubenwardy
8dcbcd8b62 Add ability for users to remove themselves as maintainers 2020-07-08 23:42:30 +01:00
rubenwardy
d00428eb7e Add info about maintainers to edit maintainers page 2020-07-08 23:28:30 +01:00
rubenwardy
0e2ea27f54 Add notifications for editing maintainers 2020-07-08 23:20:29 +01:00
rubenwardy
b2809ed12e Fix maintainers field requiring lowercase names 2020-07-08 23:00:45 +01:00
rubenwardy
a72b9a174a Add support for package maintainers
Fixes #159
2020-07-08 22:45:24 +01:00
rubenwardy
ecb3d83c57 Fix FileNotFoundError on missing thumbnail source 2020-06-25 14:58:09 +01:00
rubenwardy
2cfb59d042 Return dictionary of package to deps in API 2020-06-05 16:09:27 +01:00
rubenwardy
4c3063cadf Fix typo 2020-06-05 04:48:53 +01:00
rubenwardy
66885fedaa Fix bugs, and document 2020-06-05 04:47:50 +01:00
rubenwardy
064eb9df04 Add ability to not include optional deps in deps API 2020-06-05 04:44:39 +01:00
rubenwardy
c3cef1eed6 Return package IDs only in dependency API 2020-06-05 04:29:52 +01:00
rubenwardy
ba8c4d3d24 Fix crash in MinetestRelease.get() 2020-06-04 20:40:56 +01:00
rubenwardy
c99a2a554b Fix typos 2020-06-03 18:28:08 +01:00
rubenwardy
749e7c6cd0 Add Package Config help page 2020-06-03 18:22:23 +01:00
rubenwardy
4d29087431 Fix crash on missing x_minetest_version meta 2020-06-03 17:53:03 +01:00
rubenwardy
183b769ee2 Add support for setting min/max minetest versions in conf 2020-06-03 17:46:59 +01:00
rubenwardy
14cf3912f0 Add support for getting MinetestRelease using engine_version 2020-06-03 17:33:33 +01:00
rubenwardy
720457e876 Add helper link to API tokens page 2020-06-03 16:41:06 +01:00
rubenwardy
27d004d299 Fix bad URL construction in GitLab webhooks 2020-06-03 16:40:52 +01:00
rubenwardy
7f650a619e Fix webhook issues, make repo URLs matched case insensitive 2020-06-03 16:32:39 +01:00
rubenwardy
d7977dec84 Fix broken link to content flags in API help page 2020-05-31 21:29:55 +01:00
rubenwardy
99a8f3d5d6 Fix broken release auto-approval due to permissions check 2020-05-31 16:06:04 +01:00
rubenwardy
c1b4256d44 Add delete unused uploads admin function 2020-05-30 16:48:37 +01:00
rubenwardy
ed78a2e06f Remove non-free score penalisation 2020-05-30 15:32:50 +01:00
rubenwardy
55a90e0464 Update to make the non-free policy clearer 2020-05-29 16:09:19 +01:00
rubenwardy
fb78136870 Fix bullet points in non-free page 2020-05-29 15:56:07 +01:00
rubenwardy
b477556698 Add help page on non-free licenses 2020-05-29 15:52:30 +01:00
rubenwardy
fc5cca9def Fix display_name rather than username being used in API 2020-05-27 17:47:31 +01:00
rubenwardy
dc455bcd87 Fix wrong API error response 2020-05-27 17:47:20 +01:00
TumeniNodes
bda82d2792 Fix spelling error (#165) 2020-05-23 17:02:01 +01:00
rubenwardy
a36e233051 Fix API auth crash and add more error messages 2020-05-19 17:24:57 +01:00
rubenwardy
8484c0f0aa Fix minor security vulnerability 2020-05-19 16:46:47 +01:00
rubenwardy
ffb5b49521 Fix crash on invalid protocol_version 2020-05-19 16:39:39 +01:00
rubenwardy
c15dd183a0 Update top packages 2020-04-30 22:41:55 +01:00
rubenwardy
0eca2d49ba Add celery exporter 2020-04-24 00:49:40 +01:00
rubenwardy
57e7cbfd09 Make OpenGraph URLs absolute 2020-04-23 23:51:10 +01:00
Lars Mueller
e94bd9b845 Add meta to package view pages 2020-04-23 23:51:03 +01:00
rubenwardy
05bf8e3b3d Add prometheus support 2020-04-23 23:30:37 +01:00
rubenwardy
3992b19be3 Optimise SQL queries 2020-04-21 20:35:05 +01:00
rubenwardy
a678a61c23 Correct documentation on users allowed to use webhooks 2020-04-21 19:27:34 +01:00
rubenwardy
b5ce0a786a Improve legibility of textual content 2020-04-21 19:18:06 +01:00
rubenwardy
d58579d308 Document top packages algorithm 2020-04-21 18:26:03 +01:00
rubenwardy
0620c3e00f Add API to see scores 2020-04-21 18:15:13 +01:00
rubenwardy
a8374ec779 Allow all members to approve own releases 2020-04-21 17:07:04 +01:00
David Leal
24090235d1 Add build status badge on README.md (#194) 2020-04-20 23:01:59 +01:00
rubenwardy
bbaa687aa7 Format exception emails better 2020-04-14 14:45:06 +01:00
rubenwardy
dadfe72b48 Improve user authentication error handling 2020-04-14 14:39:59 +01:00
576 changed files with 355543 additions and 18274 deletions

View File

@@ -3,3 +3,4 @@ data*
uploads
*.pyc
__pycache__
env

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
liberapay: rubenwardy
patreon: rubenwardy
custom: [ "https://rubenwardy.com/donate/" ]

7
.github/SECURITY.md vendored
View File

@@ -2,8 +2,8 @@
## Supported Versions
We only support the latest production version, deployed to <https://content.minetest.net>.
See the [releases page](https://github.com/minetest/contentdb/releases).
We only support the latest production version, deployed to <https://content.luanti.org>.
This is usually the latest `master` commit.
## Reporting a Vulnerability
@@ -12,8 +12,5 @@ to give us time to fix them. You can do that by using one of the methods outline
* https://rubenwardy.com/contact/
Depending on severity, we will either create a private issue for the vulnerability
and release a security update, or give you permission to file the issue publicly.
For more information on the justification of this policy, see
[Responsible Disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).

23
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install docker-compose
run: sudo apt-get install -y docker-compose
- uses: actions/checkout@v4
- name: Copy config
run: cp utils/ci/* .
- name: Build the Docker image
run: docker-compose build
- name: Start Docker
run: docker-compose up -d
- name: Run migrations
run: ./utils/run_migrations.sh
- name: Run tests
run: ./utils/tests_cov.sh
- name: Stop Docker
run: docker-compose down

6
.gitignore vendored
View File

@@ -10,6 +10,8 @@ app/public/uploads
app/public/thumbnails
celerybeat-schedule
/data
.idea
*.mo
# Created by https://www.gitignore.io/api/linux,macos,python,windows
@@ -104,10 +106,6 @@ coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Flask stuff:
instance/
.webassets-cache

View File

@@ -1,22 +0,0 @@
image: docker/compose
services:
- docker:dind
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- /var/lib/docker
# build:
# stage: build
# script:
# - cp utils/gitlabci/* .
# - docker-compose build
UI_Test:
stage: test
script:
- cp utils/gitlabci/* .
- docker-compose up -d
- ./utils/run_migrations.sh
- ./utils/tests_cov.sh
- docker-compose down

View File

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

660
LICENSE.md Normal file
View File

@@ -0,0 +1,660 @@
### GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains
free software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing
under this license.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public
License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your
version supports such interaction) an opportunity to receive the
Corresponding Source of your version by providing access to the
Corresponding Source from a network server at no charge, through some
standard or customary means of facilitating copying of software. This
Corresponding Source shall include the Corresponding Source for any
work covered by version 3 of the GNU General Public License that is
incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Affero General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever
published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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/>.
Also add information on how to contact you by electronic and paper
mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for
the specific requirements.
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU AGPL, see <https://www.gnu.org/licenses/>.

View File

@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,36 +1,74 @@
# Content Database
# ContentDB
![Build Status](https://github.com/luanti-org/contentdb/actions/workflows/test.yml/badge.svg)
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 GPLv3.0+.
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
See [Developer Intro](docs/dev_intro.md) for an overview of the code organisation.
## Credits
* `app/public/static/placeholder.png`: erlehmann, Warr1024. License: CC BY-SA 3.0
## How-tos
Note: you should first read one of the guides on the [Github repo wiki](https://github.com/minetest/contentdb/wiki)
```sh
# Run celery worker
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
# if sqlite
python utils/setup.py -t
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
# Create migration
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
# Run migration
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
# Enter docker
docker exec -it contentdb_app_1 bash
# Hot/live reload (only works with FLASK_DEBUG=1)
./utils/reload.sh
# Cold update a running version of CDB with minimal downtime
# Cold update a running version of CDB with minimal downtime (production)
./utils/update.sh
# Enter docker
./utils/bash.sh
# Run migrations
./utils/run_migrations.sh
# Create new migration
./utils/create_migration.sh
# Delete database
docker-compose down && sudo rm -rf data/db
```
### VSCode: Setting up Linting
* (optional) Install the [Docker extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
* Install the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
* Click no to installing pylint (we don't want it to be installed outside a virtual env)
* Set up a virtual env
* Replace `psycopg2` with `psycopg2_binary` in requirements.txt (because postgresql won't be installed on the system)
* `python3 -m venv env`
* Click yes to prompt to select virtual env for workspace
* Click yes to any prompts about installing pylint
* `source env/bin/activate`
* `pip install -r requirements`
* `pip install pylint` (if a prompt didn't appear)
* Undo changes to requirements.txt
### VSCode: Material Icon Folder Designations
```json
"material-icon-theme.folders.associations": {
"packages": "",
"tasks": "",
"api": "",
"meta": "",
"blueprints": "routes",
"scss": "sass",
"flatpages": "markdown",
"data": "temp",
"migrations": "archive",
"textures": "images",
"sounds": "audio"
}
```
## Database
@@ -44,7 +82,7 @@ Package "1" --> "*" Release
Package "1" --> "*" Dependency
Package "1" --> "*" Tag
Package "1" --> "*" MetaPackage : provides
Release --> MinetestVersion
Release --> LuantiVersion
Package --> License
Dependency --> Package
Dependency --> MetaPackage

View File

@@ -1,94 +1,245 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
import datetime
import os
import redis
from flask import *
from flask_user import *
from flask_gravatar import Gravatar
import flask_menu as menu
from flask_mail import Mail
from flask_github import GitHub
from flask_wtf.csrf import CSRFProtect
from flask import redirect, url_for, render_template, flash, request, Flask, send_from_directory, make_response, render_template_string
from flask_babel import Babel, gettext
from flask_flatpages import FlatPages
from flask_babel import Babel
import os, redis
from flask_github import GitHub
from flask_login import logout_user, current_user, LoginManager
from flask_mail import Mail
from flask_wtf.csrf import CSRFProtect
from app.markdown import init_markdown, render_markdown
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
if os.getenv("SENTRY_DSN"):
def before_send(event, hint):
from app.tasks import TaskError
if "exc_info" in hint:
exc_type, exc_value, tb = hint["exc_info"]
if isinstance(exc_value, TaskError):
return None
return event
environment = os.getenv("SENTRY_ENVIRONMENT")
assert environment is not None
sentry_sdk.init(
dsn=os.getenv("SENTRY_DSN"),
environment=environment,
integrations=[FlaskIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
traces_sample_rate=0.1,
# Set profiles_sample_rate to 1.0 to profile 100%
# of sampled transactions.
# We recommend adjusting this value in production.
profiles_sample_rate=0.1,
before_send=before_send,
)
app = Flask(__name__, static_folder="public/static")
def my_flatpage_renderer(text):
# Render with jinja first
prerendered_body = render_template_string(text)
return render_markdown(prerendered_body, clean=False)
app.config["FLATPAGES_ROOT"] = "flatpages"
app.config["FLATPAGES_EXTENSION"] = ".md"
app.config["FLATPAGES_HTML_RENDERER"] = my_flatpage_renderer
app.config["WTF_CSRF_TIME_LIMIT"] = None
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "../translations"
app.config["LANGUAGES"] = {
"en": "English",
"cs": "čeština",
"de": "Deutsch",
"es": "Español",
"fr": "Français",
"id": "Bahasa Indonesia",
"it": "Italiano",
"ms": "Bahasa Melayu",
"pl": "Język Polski",
"ru": "русский язык",
"sk": "Slovenčina",
"sv": "Svenska",
"ta": "தமிழ்",
"tr": "Türkçe",
"uk": "Українська",
"vi": "tiếng Việt",
"zh_CN": "汉语",
}
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
r = redis.Redis.from_url(app.config["REDIS_URL"])
if not app.config["ADMIN_CONTACT_URL"]:
raise Exception("Missing config property: ADMIN_CONTACT_URL")
redis_client = redis.Redis.from_url(app.config["REDIS_URL"])
menu.Menu(app=app)
github = GitHub(app)
csrf = CSRFProtect(app)
mail = Mail(app)
pages = FlatPages(app)
babel = Babel(app)
gravatar = Gravatar(app,
size=58,
rating='g',
default='mp',
force_default=False,
force_lower=False,
use_ssl=True,
base_url=None)
babel = Babel()
init_markdown(app)
from .sass import sass
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "users.login"
from .sass import init_app as sass
sass(app)
if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
from .maillogger import register_mail_error_handler
register_mail_error_handler(app, mail)
from . import models, template_filters
from .markdown import init_app
init_app(app)
@login_manager.user_loader
def load_user(user_id):
return models.User.query.filter_by(username=user_id).first()
# @babel.localeselector
# def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
from . import models, tasks, template_filters
from .blueprints import create_blueprints
create_blueprints(app)
from flask_login import logout_user
@app.route("/uploads/<path:path>")
def send_upload(path):
return send_from_directory(app.config['UPLOAD_DIR'], path)
return send_from_directory(app.config["UPLOAD_DIR"], path)
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
@app.route('/<path:path>/')
@app.route("/<path:path>/")
def flatpage(path):
page = pages.get_or_404(path)
template = page.meta.get('template', 'flatpage.html')
return render_template(template, page=page)
page = pages.get_or_404(path)
template = page.meta.get("template", "flatpage.html")
return render_template(template, page=page)
@app.before_request
def check_for_ban():
if current_user.is_authenticated:
if current_user.rank == models.UserRank.BANNED:
flash("You have been banned.", "danger")
logout_user()
return redirect(url_for('user.login'))
elif current_user.rank == models.UserRank.NOT_JOINED:
current_user.rank = models.UserRank.MEMBER
if current_user.ban and current_user.ban.has_expired:
models.db.session.delete(current_user.ban)
if current_user.rank == models.UserRank.BANNED:
current_user.rank = models.UserRank.MEMBER
models.db.session.commit()
elif current_user.is_banned:
if current_user.ban:
flash(gettext("Banned:") + " " + current_user.ban.message, "danger")
else:
flash(gettext("You have been banned."), "danger")
logout_user()
return redirect(url_for("users.login"))
elif current_user.rank == models.UserRank.NOT_JOINED:
current_user.rank = models.UserRank.NEW_MEMBER
models.db.session.commit()
from .utils import clear_notifications, is_safe_url, create_session
@app.before_request
def check_for_notifications():
clear_notifications(request.path)
@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404
@app.errorhandler(500)
def server_error(e):
return render_template("500.html"), 500
def get_locale():
if not request:
return None
locales = app.config["LANGUAGES"].keys()
if current_user.is_authenticated and current_user.locale in locales:
return current_user.locale
locale = request.cookies.get("locale")
if locale not in locales:
locale = request.accept_languages.best_match(locales)
if locale and current_user.is_authenticated:
with create_session() as new_session:
new_session.query(models.User) \
.filter(models.User.username == current_user.username) \
.update({"locale": locale})
new_session.commit()
return locale
babel.init_app(app, locale_selector=get_locale)
@app.route("/set-locale/", methods=["POST"])
@csrf.exempt
def set_locale():
locale = request.form.get("locale")
if locale not in app.config["LANGUAGES"].keys():
flash("Unknown locale {}".format(locale), "danger")
locale = None
next_url = request.form.get("r")
if next_url and is_safe_url(next_url):
resp = make_response(redirect(next_url))
else:
resp = make_response(redirect(url_for("homepage.home")))
if locale:
expire_date = datetime.datetime.now()
expire_date = expire_date + datetime.timedelta(days=5*365)
resp.set_cookie("locale", locale, expires=expire_date, secure=True, samesite="Lax")
if current_user.is_authenticated:
current_user.locale = locale
models.db.session.commit()
return resp
@app.route("/set-nonfree/", methods=["POST"])
def set_nonfree():
resp = redirect(url_for("homepage.home"))
if request.cookies.get("hide_nonfree") == "1":
resp.set_cookie("hide_nonfree", "0", expires=0, secure=True, samesite="Lax")
else:
expire_date = datetime.datetime.now()
expire_date = expire_date + datetime.timedelta(days=5*365)
resp.set_cookie("hide_nonfree", "1", expires=expire_date, secure=True, samesite="Lax")
return resp

252
app/_translations.py Normal file
View File

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

View File

@@ -1,4 +1,22 @@
import os, importlib
# ContentDB
# Copyright (C) 2018-21 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 importlib
import os
def create_blueprints(app):
dir = os.path.dirname(os.path.realpath(__file__))

View File

@@ -1,17 +1,17 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
@@ -19,4 +19,4 @@ from flask import Blueprint
bp = Blueprint("admin", __name__)
from . import admin, licenseseditor, tagseditor, versioneditor
from . import admin, audit, licenseseditor, tagseditor, versioneditor, warningseditor, languageseditor, email, approval_stats

View File

@@ -0,0 +1,434 @@
# ContentDB
# Copyright (C) 2018-21 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 datetime
import os
from typing import List
import requests
from celery import group, uuid
from flask import redirect, url_for, flash, current_app
from sqlalchemy import or_, and_, not_, func
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry, ReportAttachment
from app.tasks.emails import send_pending_digests
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
import_languages, check_all_zip_files
from app.tasks.usertasks import import_github_user_ids, do_delete_likely_spammers
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links, update_file_size_bytes
from app.utils import add_notification, get_system_user
actions = {}
def action(title: str):
def func(f):
name = f.__name__
actions[name] = {
"title": title,
"func": f,
}
return f
return func
@action("Delete stuck releases")
def del_stuck_releases():
PackageRelease.query.filter(PackageRelease.task_id.isnot(None)).delete()
db.session.commit()
return redirect(url_for("admin.admin_page"))
@action("Delete unused uploads")
def clean_uploads():
upload_dir = current_app.config['UPLOAD_DIR']
(_, _, filenames) = next(os.walk(upload_dir))
existing_uploads = set(filenames)
if len(existing_uploads) != 0:
def get_filenames_from_column(column):
results = db.session.query(column).filter(column.isnot(None), column != "").all()
return set([os.path.basename(x[0]) for x in results])
release_urls = get_filenames_from_column(PackageRelease.url)
screenshot_urls = get_filenames_from_column(PackageScreenshot.url)
attachment_urls = get_filenames_from_column(ReportAttachment.url)
pp_urls = get_filenames_from_column(User.profile_pic)
db_urls = release_urls.union(screenshot_urls).union(pp_urls).union(attachment_urls)
unreachable = existing_uploads.difference(db_urls)
import sys
print("On Disk: ", existing_uploads, file=sys.stderr)
print("In DB: ", db_urls, file=sys.stderr)
print("Unreachable: ", unreachable, file=sys.stderr)
for filename in unreachable:
os.remove(os.path.join(upload_dir, filename))
flash("Deleted " + str(len(unreachable)) + " unreachable uploads", "success")
else:
flash("No downloads to create", "danger")
return redirect(url_for("admin.admin_page"))
@action("Delete unused mod names")
def del_mod_names():
query = MetaPackage.query.filter(~MetaPackage.dependencies.any(), ~MetaPackage.packages.any())
count = query.count()
query.delete(synchronize_session=False)
db.session.commit()
flash("Deleted " + str(count) + " unused mod names", "success")
return redirect(url_for("admin.admin_page"))
@action("Recalc package scores")
def recalc_scores():
for package in Package.query.all():
package.recalculate_score()
db.session.commit()
flash("Recalculated package scores", "success")
return redirect(url_for("admin.admin_page"))
@action("Import forum topic list")
def do_import_topic_list():
task = import_topic_list.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Check all forum accounts")
def check_all_forum_accounts():
task = check_all_forum_accounts.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Run update configs")
def run_update_config():
check_for_updates.delay()
flash("Started update configs", "success")
return redirect(url_for("admin.admin_page"))
def _package_list(packages: List[str]):
# Who needs translations?
if len(packages) >= 3:
packages[len(packages) - 1] = "and " + packages[len(packages) - 1]
return ", ".join(packages)
else:
return " and ".join(packages)
@action("Send WIP package notification")
def remind_wip():
users = User.query.filter(User.packages.any(or_(
Package.state == PackageState.WIP, Package.state == PackageState.CHANGES_NEEDED)))
system_user = get_system_user()
for user in users:
packages = Package.query.filter(
Package.author_id == user.id,
or_(Package.state == PackageState.WIP, Package.state == PackageState.CHANGES_NEEDED)) \
.all()
packages = [pkg.title for pkg in packages]
packages_list = _package_list(packages)
havent = "haven't" if len(packages) > 1 else "hasn't"
add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"Did you forget? {packages_list} {havent} been submitted for review yet",
url_for('todo.view_user', username=user.username))
db.session.commit()
@action("Send outdated package notification")
def remind_outdated():
users = User.query.filter(User.maintained_packages.any(
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))))
system_user = get_system_user()
for user in users:
packages = Package.query.filter(
Package.maintainers.contains(user),
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
.all()
packages = [pkg.title for pkg in packages]
packages_list = _package_list(packages)
add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"The following packages may be outdated: {packages_list}",
url_for('todo.view_user', username=user.username))
db.session.commit()
@action("Import licenses from SPDX")
def import_licenses():
renames = {
"GPLv2": "GPL-2.0-only",
"GPLv3": "GPL-3.0-only",
"AGPLv2": "AGPL-2.0-only",
"AGPLv3": "AGPL-3.0-only",
"LGPLv2.1": "LGPL-2.1-only",
"LGPLv3": "LGPL-3.0-only",
"Apache 2.0": "Apache-2.0",
"BSD 2-Clause / FreeBSD": "BSD-2-Clause-FreeBSD",
"BSD 3-Clause": "BSD-3-Clause",
"CC0": "CC0-1.0",
"CC BY 3.0": "CC-BY-3.0",
"CC BY 4.0": "CC-BY-4.0",
"CC BY-NC-SA 3.0": "CC-BY-NC-SA-3.0",
"CC BY-SA 3.0": "CC-BY-SA-3.0",
"CC BY-SA 4.0": "CC-BY-SA-4.0",
"NPOSLv3": "NPOSL-3.0",
"MPL 2.0": "MPL-2.0",
"EUPLv1.2": "EUPL-1.2",
"SIL Open Font License v1.1": "OFL-1.1",
}
for old_name, new_name in renames.items():
License.query.filter_by(name=old_name).update({ "name": new_name })
r = requests.get(
"https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json")
licenses = r.json()["licenses"]
existing_licenses = {}
for license_data in License.query.all():
assert license_data.name not in renames.keys()
existing_licenses[license_data.name.lower()] = license_data
for license_data in licenses:
obj = existing_licenses.get(license_data["licenseId"].lower())
if obj:
obj.url = license_data["reference"]
elif license_data.get("isOsiApproved") and license_data.get("isFsfLibre") and not license_data["isDeprecatedLicenseId"]:
obj = License(license_data["licenseId"], True, license_data["reference"])
db.session.add(obj)
db.session.commit()
@action("Delete inactive users")
def delete_inactive_users():
users = User.query.filter(User.is_active == False, ~User.packages.any(), ~User.forum_topics.any(),
User.rank == UserRank.NOT_JOINED).all()
for user in users:
db.session.delete(user)
db.session.commit()
@action("Send Video URL notification")
def remind_video_url():
users = User.query.filter(User.maintained_packages.any(
and_(Package.video_url == None, Package.type == PackageType.GAME, Package.state == PackageState.APPROVED)))
system_user = get_system_user()
for user in users:
packages = Package.query.filter(
or_(Package.author == user, Package.maintainers.contains(user)),
Package.video_url == None,
Package.type == PackageType.GAME,
Package.state == PackageState.APPROVED) \
.all()
package_names = [pkg.title for pkg in packages]
packages_list = _package_list(package_names)
add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"You should add a video to {packages_list}",
url_for('users.profile', username=user.username))
db.session.commit()
@action("Send missing game support notifications")
def remind_missing_game_support():
users = User.query.filter(
User.maintained_packages.any(and_(
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False))).all()
system_user = get_system_user()
for user in users:
packages = Package.query.filter(
Package.maintainers.contains(user),
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False) \
.all()
packages = [pkg.title for pkg in packages]
packages_list = _package_list(packages)
add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"You need to confirm whether the following packages support all games: {packages_list}",
url_for('todo.all_game_support', username=user.username))
db.session.commit()
@action("Detect game support")
def detect_game_support():
task_id = uuid()
update_all_game_support.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("Send pending notif digests")
def do_send_pending_digests():
send_pending_digests.delay()
@action("Import user ids from GitHub")
def do_import_github_user_ids():
task_id = uuid()
import_github_user_ids.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("Notify about links to git/forums instead of CDB")
def do_notify_git_forums_links():
task_id = uuid()
notify_about_git_forum_links.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("Check all zip files")
def do_check_all_zip_files():
task_id = uuid()
check_all_zip_files.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("Update file_size_bytes")
def do_update_file_size_bytes():
task_id = uuid()
update_file_size_bytes.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("DANGER: Delete less popular removed packages")
def del_less_popular_removed_packages():
task_id = uuid()
clear_removed_packages.apply_async((False, ), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("DANGER: Delete all removed packages")
def del_removed_packages():
task_id = uuid()
clear_removed_packages.apply_async((True, ), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
@action("DANGER: Check all releases (postReleaseCheckUpdate)")
def check_releases():
releases = PackageRelease.query.filter(PackageRelease.url.like("/uploads/%")).all()
tasks = []
for release in releases:
tasks.append(check_zip_release.s(release.id, release.file_path))
result = group(tasks).apply_async()
while not result.ready():
import time
time.sleep(0.1)
return redirect(url_for("todo.view_editor"))
@action("DANGER: Check latest release of all packages (postReleaseCheckUpdate)")
def reimport_packages():
tasks = []
for package in Package.query.filter(Package.state == PackageState.APPROVED).all():
release = package.releases.first()
if release:
tasks.append(check_zip_release.s(release.id, release.file_path))
result = group(tasks).apply_async()
while not result.ready():
import time
time.sleep(0.1)
return redirect(url_for("todo.view_editor"))
@action("DANGER: Import translations")
def reimport_translations():
tasks = []
for package in Package.query.filter(Package.state == PackageState.APPROVED).all():
release = package.releases.first()
if release:
tasks.append(import_languages.s(release.id, release.file_path))
result = group(tasks).apply_async()
while not result.ready():
import time
time.sleep(0.1)
return redirect(url_for("todo.view_editor"))
@action("DANGER: Import screenshots from Git")
def import_screenshots():
packages = Package.query \
.filter(Package.state != PackageState.DELETED) \
.outerjoin(PackageScreenshot, Package.id == PackageScreenshot.package_id) \
.filter(PackageScreenshot.id == None) \
.all()
for package in packages:
import_repo_screenshot.delay(package.id)
return redirect(url_for("admin.admin_page"))
@action("DANGER: Delete empty threads")
def delete_empty_threads():
query = Thread.query.filter(~Thread.replies.any())
count = query.count()
for thread in query.all():
thread.watchers.clear()
db.session.delete(thread)
db.session.commit()
flash(f"Deleted {count} threads", "success")
return redirect(url_for("admin.admin_page"))
@action("DANGER: Check for broken links in all packages")
def check_for_broken_links():
for package in Package.query.filter_by(state=PackageState.APPROVED).all():
check_package_for_broken_links.delay(package.id)
@action("DANGER: Delete likely spammers")
def delete_likely_spammers():
task_id = uuid()
do_delete_likely_spammers.apply_async((), task_id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))

View File

@@ -1,124 +1,48 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
from flask_user import *
import flask_menu as menu
from . import bp
from app.models import *
from celery import uuid, group
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease, checkZipRelease
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from flask import redirect, render_template, url_for, request, flash
from flask_login import current_user, login_user
from flask_wtf import FlaskForm
from wtforms import *
from app.utils import loginUser, rank_required, triggerNotif
import datetime
from wtforms import StringField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length, Optional
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
get_int_or_abort
from sqlalchemy import func
from . import bp
from .actions import actions
from app.models import UserRank, Package, db, PackageState, PackageRelease, PackageScreenshot, User, AuditSeverity, NotificationType, PackageAlias
from ...querybuilder import QueryBuilder
@bp.route("/admin/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
@rank_required(UserRank.EDITOR)
def admin_page():
if request.method == "POST":
if request.method == "POST" and current_user.rank.at_least(UserRank.ADMIN):
action = request.form["action"]
if action == "delstuckreleases":
PackageRelease.query.filter(PackageRelease.task_id != None).delete()
db.session.commit()
return redirect(url_for("admin.admin_page"))
elif action == "checkreleases":
releases = PackageRelease.query.filter(PackageRelease.url.like("/uploads/%")).all()
tasks = []
for release in releases:
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
tasks.append(checkZipRelease.s(release.id, zippath))
result = group(tasks).apply_async()
while not result.ready():
import time
time.sleep(0.1)
return redirect(url_for("todo.view"))
elif action == "importmodlist":
task = importTopicList.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
elif action == "checkusers":
task = checkAllForumAccounts.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
elif action == "importscreenshots":
packages = Package.query \
.filter_by(soft_deleted=False) \
.outerjoin(PackageScreenshot, Package.id==PackageScreenshot.package_id) \
.filter(PackageScreenshot.id==None) \
.all()
for package in packages:
importRepoScreenshot.delay(package.id)
return redirect(url_for("admin.admin_page"))
elif action == "restore":
package = Package.query.get(request.form["package"])
if package is None:
flash("Unknown package", "danger")
else:
package.soft_deleted = False
db.session.commit()
return redirect(url_for("admin.admin_page"))
elif action == "importdepends":
task = importAllDependencies.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
elif action == "modprovides":
packages = Package.query.filter_by(type=PackageType.MOD).all()
mpackage_cache = {}
for p in packages:
if len(p.provides) == 0:
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
db.session.commit()
return redirect(url_for("admin.admin_page"))
elif action == "recalcscores":
for p in Package.query.all():
p.setStartScore()
db.session.commit()
return redirect(url_for("admin.admin_page"))
elif action == "vcsrelease":
for package in Package.query.filter(Package.repo.isnot(None)).all():
if package.releases.count() != 0:
continue
rel = PackageRelease()
rel.package = package
rel.title = datetime.date.today().isoformat()
rel.url = ""
rel.task_id = uuid()
rel.approved = True
db.session.add(rel)
db.session.commit()
makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id)
msg = "{}: Release {} created".format(package.title, rel.title)
triggerNotif(package.author, current_user, msg, rel.getEditURL())
db.session.commit()
if action in actions:
ret = actions[action]["func"]()
if ret:
return ret
else:
flash("Unknown action: " + action, "danger")
deleted_packages = Package.query.filter_by(soft_deleted=True).all()
return render_template("admin/list.html", deleted_packages=deleted_packages)
return render_template("admin/list.html", actions=actions)
class SwitchUserForm(FlaskForm):
username = StringField("Username")
@@ -129,15 +53,171 @@ class SwitchUserForm(FlaskForm):
@rank_required(UserRank.ADMIN)
def switch_user():
form = SwitchUserForm(formdata=request.form)
if request.method == "POST" and form.validate():
user = User.query.filter_by(username=form["username"].data).first()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None:
flash("Unable to find user", "danger")
elif loginUser(user):
elif login_user(user):
return redirect(url_for("users.profile", username=current_user.username))
else:
flash("Unable to login as user", "danger")
# Process GET or invalid POST
return render_template("admin/switch_user.html", form=form)
class SendNotificationForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 300)])
url = StringField("URL", [InputRequired(), Length(1, 100)], default="/")
submit = SubmitField("Send")
@bp.route("/admin/send-notification/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def send_bulk_notification():
form = SendNotificationForm(request.form)
if form.validate_on_submit():
add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent bulk notification", url_for("admin.admin_page"), None, form.title.data)
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).all()
add_notification(users, get_system_user(), NotificationType.OTHER, form.title.data, form.url.data, None)
db.session.commit()
return redirect(url_for("admin.admin_page"))
return render_template("admin/send_bulk_notification.html", form=form)
@bp.route("/admin/restore/", methods=["GET", "POST"])
@rank_required(UserRank.EDITOR)
def restore():
if request.method == "POST":
target = request.form["submit"]
if "Review" in target:
target = PackageState.READY_FOR_REVIEW
elif "Changes" in target:
target = PackageState.CHANGES_NEEDED
else:
target = PackageState.WIP
package = Package.query.get(request.form["package"])
if package is None:
flash("Unknown package", "danger")
else:
package.state = target
add_audit_log(AuditSeverity.EDITOR, current_user, f"Restored package to state {target.value}",
package.get_url("packages.view"), package)
db.session.commit()
return redirect(package.get_url("packages.view"))
deleted_packages = Package.query \
.filter(Package.state == PackageState.DELETED) \
.join(Package.author) \
.order_by(db.asc(User.username), db.asc(Package.name)) \
.all()
return render_template("admin/restore.html", deleted_packages=deleted_packages)
class TransferPackageForm(FlaskForm):
old_username = StringField("Old Username", [InputRequired()])
new_username = StringField("New Username", [InputRequired()])
package = StringField("Package", [Optional()])
remove_maintainer = BooleanField("Remove current owner from maintainers")
submit = SubmitField("Transfer")
def perform_transfer(form: TransferPackageForm):
query = Package.query.filter(Package.author.has(username=form.old_username.data))
if nonempty_or_none(form.package.data):
query = query.filter_by(name=form.package.data)
packages = query.all()
if len(packages) == 0:
flash("Unable to find package(s)", "danger")
return
new_user = User.query.filter_by(username=form.new_username.data).first()
if new_user is None:
flash("Unable to find new user", "danger")
return
names = [x.name for x in packages]
already_existing = Package.query.filter(Package.author_id == new_user.id, Package.name.in_(names)).all()
if len(already_existing) > 0:
existing_names = [x.name for x in already_existing]
flash("Unable to transfer packages as names exist at destination: " + ", ".join(existing_names), "danger")
return
for package in packages:
if form.remove_maintainer.data:
package.maintainers.remove(package.author)
package.author = new_user
package.maintainers.append(new_user)
package.aliases.append(PackageAlias(form.old_username.data, package.name))
add_audit_log(AuditSeverity.MODERATION, current_user,
f"Transferred {form.old_username.data}/{package.name} to {form.new_username.data}",
package.get_url("packages.view"), package)
db.session.commit()
flash("Transferred " + ", ".join([x.name for x in packages]), "success")
return redirect(url_for("admin.transfer"))
@bp.route("/admin/transfer/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def transfer():
form = TransferPackageForm(formdata=request.form)
if form.validate_on_submit():
ret = perform_transfer(form)
if ret is not None:
return ret
# Process GET or invalid POST
return render_template("admin/transfer.html", form=form)
def sum_file_sizes(clazz):
ret = {}
for entry in (db.session
.query(clazz.package_id, func.sum(clazz.file_size_bytes))
.select_from(clazz)
.group_by(clazz.package_id)
.all()):
ret[entry[0]] = entry[1]
return ret
@bp.route("/admin/storage/")
@rank_required(UserRank.EDITOR)
def storage():
qb = QueryBuilder(request.args, cookies=True)
qb.only_approved = False
packages = qb.build_package_query().all()
show_all = len(packages) < 100
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50)
package_size_releases = sum_file_sizes(PackageRelease)
package_size_screenshots = sum_file_sizes(PackageScreenshot)
data = []
for package in packages:
size_releases = package_size_releases.get(package.id, 0)
size_screenshots = package_size_screenshots.get(package.id, 0)
size_total = size_releases + size_screenshots
if size_total < min_size * 1024 * 1024:
continue
latest_release = package.releases.first()
size_latest = latest_release.file_size_bytes if latest_release else 0
data.append([package, size_total, size_releases, size_screenshots, size_latest])
data.sort(key=lambda x: x[1], reverse=True)
return render_template("admin/storage.html", data=data)

View File

@@ -0,0 +1,77 @@
# ContentDB
# Copyright (C) 2024 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 datetime
from flask import render_template, request, abort, redirect, url_for, jsonify
from . import bp
from app.logic.approval_stats import get_approval_statistics
from app.models import UserRank
from app.utils import rank_required
@bp.route("/admin/approval_stats/")
@rank_required(UserRank.APPROVER)
def approval_stats():
start = request.args.get("start")
end = request.args.get("end")
if start and end:
try:
start = datetime.datetime.fromisoformat(start)
end = datetime.datetime.fromisoformat(end)
except ValueError:
abort(400)
elif start:
return redirect(url_for("admin.approval_stats", start=start, end=datetime.datetime.utcnow().date().isoformat()))
elif end:
return redirect(url_for("admin.approval_stats", start="2020-07-01", end=end))
else:
end = datetime.datetime.utcnow()
start = end - datetime.timedelta(days=365)
stats = get_approval_statistics(start, end)
return render_template("admin/approval_stats.html", stats=stats, start=start, end=end)
@bp.route("/admin/approval_stats.json")
@rank_required(UserRank.APPROVER)
def approval_stats_json():
start = request.args.get("start")
end = request.args.get("end")
if start and end:
try:
start = datetime.datetime.fromisoformat(start)
end = datetime.datetime.fromisoformat(end)
except ValueError:
abort(400)
else:
end = datetime.datetime.utcnow()
start = end - datetime.timedelta(days=365)
stats = get_approval_statistics(start, end)
for key, value in stats.packages_info.items():
stats.packages_info[key] = value.__dict__()
return jsonify({
"start": start.isoformat(),
"end": end.isoformat(),
"editor_approvals": stats.editor_approvals,
"packages_info": stats.packages_info,
"turnaround_time": {
"avg": stats.avg_turnaround_time,
"max": stats.max_turnaround_time,
},
})

View File

@@ -0,0 +1,73 @@
# ContentDB
# Copyright (C) 2020 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 flask import render_template, request, abort
from flask_babel import lazy_gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import Optional, Length
from app.models import db, AuditLogEntry, UserRank, User, Permission
from app.utils import rank_required, get_int_or_abort
from . import bp
class AuditForm(FlaskForm):
username = StringField(lazy_gettext("Username"), [Optional(), Length(0, 25)])
q = StringField(lazy_gettext("Query"), [Optional(), Length(0, 300)])
url = StringField(lazy_gettext("URL"), [Optional(), Length(0, 300)])
submit = SubmitField(lazy_gettext("Search"), name=None)
@bp.route("/admin/audit/")
@rank_required(UserRank.APPROVER)
def audit():
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 100))
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
form = AuditForm(request.args)
username = form.username.data
q = form.q.data
url = form.url.data
if username:
user = User.query.filter_by(username=username).first_or_404()
query = query.filter_by(causer=user)
if q:
query = query.filter(AuditLogEntry.title.ilike(f"%{q}%"))
if url:
query = query.filter(AuditLogEntry.url.ilike(f"%{url}%"))
if not current_user.rank.at_least(UserRank.MODERATOR):
query = query.filter(AuditLogEntry.package)
pagination = query.paginate(page=page, per_page=num)
return render_template("admin/audit.html", log=pagination.items, pagination=pagination, form=form)
@bp.route("/admin/audit/<int:id_>/")
@login_required
def audit_view(id_):
entry: AuditLogEntry = AuditLogEntry.query.get_or_404(id_)
if not entry.check_perm(current_user, Permission.VIEW_AUDIT_DESCRIPTION):
abort(403)
return render_template("admin/audit_view.html", entry=entry)

View File

@@ -0,0 +1,77 @@
# ContentDB
# Copyright (C) 2020 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 flask import request, abort, url_for, redirect, render_template, flash
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField, StringField
from wtforms.validators import InputRequired, Length
from app.markdown import render_markdown
from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk
from app.utils import rank_required, add_audit_log, normalize_line_endings
from . import bp
from app.models import UserRank, User, AuditSeverity
class SendEmailForm(FlaskForm):
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
text = TextAreaField("Message", [InputRequired()], filters=[normalize_line_endings])
submit = SubmitField("Send")
@bp.route("/admin/send-email/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def send_single_email():
username = request.args["username"]
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
next_url = url_for("users.profile", username=user.username)
if user.email is None:
flash("User has no email address!", "danger")
return redirect(next_url)
form = SendEmailForm(request.form)
if form.validate_on_submit():
add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent email to {}".format(user.display_name), url_for("users.profile", username=username))
text = form.text.data
html = render_markdown(text)
task = send_user_email.delay(user.email, user.locale or "en", form.subject.data, text, html)
return redirect(url_for("tasks.check", id=task.id, r=next_url))
return render_template("admin/send_email.html", form=form, user=user)
@bp.route("/admin/send-bulk-email/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def send_bulk_email():
form = SendEmailForm(request.form)
if form.validate_on_submit():
add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent bulk email", url_for("admin.admin_page"), None, form.text.data)
text = form.text.data
html = render_markdown(text)
task_send_bulk.delay(form.subject.data, text, html)
return redirect(url_for("admin.admin_page"))
return render_template("admin/send_bulk_email.html", form=form)

View File

@@ -0,0 +1,73 @@
# ContentDB
# Copyright (C) 2018-24 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 flask import redirect, render_template, abort, url_for
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length, Optional
from app.models import db, AuditSeverity, UserRank, Language, Package, PackageState, PackageTranslation
from app.utils import add_audit_log, rank_required, normalize_line_endings
from . import bp
@bp.route("/admin/languages/")
@rank_required(UserRank.ADMIN)
def language_list():
at_least_one_count = db.session.query(PackageTranslation.package_id).group_by(PackageTranslation.package_id).count()
total_package_count = Package.query.filter_by(state=PackageState.APPROVED).count()
return render_template("admin/languages/list.html",
languages=Language.query.all(), total_package_count=total_package_count,
at_least_one_count=at_least_one_count)
class LanguageForm(FlaskForm):
id = StringField("Id", [InputRequired(), Length(2, 10)])
title = TextAreaField("Title", [Optional(), Length(2, 100)], filters=[normalize_line_endings])
submit = SubmitField("Save")
@bp.route("/admin/languages/new/", methods=["GET", "POST"])
@bp.route("/admin/languages/<id_>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def create_edit_language(id_=None):
language = None
if id_ is not None:
language = Language.query.filter_by(id=id_).first()
if language is None:
abort(404)
form = LanguageForm(obj=language)
if form.validate_on_submit():
if language is None:
language = Language()
db.session.add(language)
form.populate_obj(language)
add_audit_log(AuditSeverity.EDITOR, current_user, f"Created language {language.id}",
url_for("admin.create_edit_language", id_=language.id))
else:
form.populate_obj(language)
add_audit_log(AuditSeverity.EDITOR, current_user, f"Edited language {language.id}",
url_for("admin.create_edit_language", id_=language.id))
db.session.commit()
return redirect(url_for("admin.create_edit_language", id_=language.id))
return render_template("admin/languages/edit.html", language=language, form=form)

View File

@@ -1,38 +1,43 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
from flask_user import *
from . import bp
from app.models import *
from flask import redirect, render_template, abort, url_for, request, flash
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required
from wtforms import StringField, BooleanField, SubmitField, URLField
from wtforms.validators import InputRequired, Length, Optional
from app.utils import rank_required, nonempty_or_none, add_audit_log
from . import bp
from app.models import UserRank, License, db, AuditSeverity
@bp.route("/licenses/")
@rank_required(UserRank.MODERATOR)
def license_list():
return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all())
class LicenseForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3,100)])
is_foss = BooleanField("Is FOSS")
submit = SubmitField("Save")
name = StringField("Name", [InputRequired(), Length(3, 100)])
is_foss = BooleanField("Is FOSS")
url = URLField("URL", [Optional()], filters=[nonempty_or_none])
submit = SubmitField("Save")
@bp.route("/licenses/new/", methods=["GET", "POST"])
@bp.route("/licenses/<name>/edit/", methods=["GET", "POST"])
@@ -47,14 +52,20 @@ def create_edit_license(name=None):
form = LicenseForm(formdata=request.form, obj=license)
if request.method == "GET" and license is None:
form.is_foss.data = True
elif request.method == "POST" and form.validate():
elif form.validate_on_submit():
if license is None:
license = License(form.name.data)
db.session.add(license)
flash("Created license " + form.name.data, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, f"Created license {license.name}",
url_for("admin.license_list"))
else:
flash("Updated license " + form.name.data, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited license {license.name}",
url_for("admin.license_list"))
form.populate_obj(license)
db.session.commit()
return redirect(url_for("admin.license_list"))

View File

@@ -1,42 +1,58 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
from flask_user import *
from . import bp
from app.models import *
from flask import redirect, render_template, abort, url_for, request
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required
from wtforms import StringField, TextAreaField, BooleanField, SubmitField
from wtforms.validators import InputRequired, Length, Optional, Regexp
from . import bp
from app.models import Permission, Tag, db, AuditSeverity
from app.utils import add_audit_log, normalize_line_endings
@bp.route("/tags/")
@rank_required(UserRank.MODERATOR)
@login_required
def tag_list():
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
if not Permission.EDIT_TAGS.check(current_user):
abort(403)
query = Tag.query
if request.args.get("sort") == "views":
query = query.order_by(db.desc(Tag.views))
else:
query = query.order_by(db.asc(Tag.title))
return render_template("admin/tags/list.html", tags=query.all())
class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)])
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save")
title = StringField("Title", [InputRequired(), Length(3, 100)])
description = TextAreaField("Description", [Optional(), Length(0, 500)], filters=[normalize_line_endings])
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0,
"Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save")
@bp.route("/tags/new/", methods=["GET", "POST"])
@bp.route("/tags/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
@login_required
def create_edit_tag(name=None):
tag = None
if name is not None:
@@ -44,14 +60,29 @@ def create_edit_tag(name=None):
if tag is None:
abort(404)
form = TagForm(formdata=request.form, obj=tag)
if request.method == "POST" and form.validate():
if not Permission.check_perm(current_user, Permission.EDIT_TAGS if tag else Permission.CREATE_TAG):
abort(403)
form = TagForm(obj=tag)
if form.validate_on_submit():
if tag is None:
tag = Tag(form.title.data)
tag.description = form.description.data
db.session.add(tag)
add_audit_log(AuditSeverity.EDITOR, current_user, f"Created tag {tag.name}",
url_for("admin.create_edit_tag", name=tag.name))
else:
form.populate_obj(tag)
add_audit_log(AuditSeverity.EDITOR, current_user, f"Edited tag {tag.name}",
url_for("admin.create_edit_tag", name=tag.name))
db.session.commit()
return redirect(url_for("admin.create_edit_tag", name=tag.name))
if Permission.EDIT_TAGS.check(current_user):
return redirect(url_for("admin.create_edit_tag", name=tag.name))
else:
return redirect(url_for("homepage.home"))
return render_template("admin/tags/edit.html", tag=tag, form=form)

View File

@@ -1,38 +1,43 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
from flask_user import *
from . import bp
from app.models import *
from flask import redirect, render_template, abort, url_for, request, flash
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required
from wtforms import StringField, IntegerField, SubmitField
from wtforms.validators import InputRequired, Length
from app.utils import rank_required, add_audit_log
from . import bp
from app.models import UserRank, LuantiRelease, db, AuditSeverity
@bp.route("/versions/")
@rank_required(UserRank.MODERATOR)
def version_list():
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
return render_template("admin/versions/list.html",
versions=LuantiRelease.query.order_by(db.asc(LuantiRelease.id)).all())
class VersionForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3,100)])
name = StringField("Name", [InputRequired(), Length(3, 100)])
protocol = IntegerField("Protocol")
submit = SubmitField("Save")
submit = SubmitField("Save")
@bp.route("/versions/new/", methods=["GET", "POST"])
@bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
@@ -40,19 +45,25 @@ class VersionForm(FlaskForm):
def create_edit_version(name=None):
version = None
if name is not None:
version = MinetestRelease.query.filter_by(name=name).first()
version = LuantiRelease.query.filter_by(name=name).first()
if version is None:
abort(404)
form = VersionForm(formdata=request.form, obj=version)
if request.method == "POST" and form.validate():
if form.validate_on_submit():
if version is None:
version = MinetestRelease(form.name.data)
version = LuantiRelease(form.name.data)
db.session.add(version)
flash("Created version " + form.name.data, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, f"Created version {version.name}",
url_for("admin.license_list"))
else:
flash("Updated version " + form.name.data, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited version {version.name}",
url_for("admin.version_list"))
form.populate_obj(version)
db.session.commit()
return redirect(url_for("admin.version_list"))

View File

@@ -0,0 +1,63 @@
# ContentDB
# Copyright (C) 2020 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 flask import redirect, render_template, abort, url_for, request
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length, Optional, Regexp
from app.utils import rank_required, normalize_line_endings
from . import bp
from app.models import UserRank, ContentWarning, db
@bp.route("/admin/warnings/")
@rank_required(UserRank.ADMIN)
def warning_list():
return render_template("admin/warnings/list.html", warnings=ContentWarning.query.order_by(db.asc(ContentWarning.title)).all())
class WarningForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3, 100)])
description = TextAreaField("Description", [Optional(), Length(0, 500)], filters=[normalize_line_endings])
name = StringField("Name", [Optional(), Length(1, 20),
Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save")
@bp.route("/admin/warnings/new/", methods=["GET", "POST"])
@bp.route("/admin/warnings/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def create_edit_warning(name=None):
warning = None
if name is not None:
warning = ContentWarning.query.filter_by(name=name).first()
if warning is None:
abort(404)
form = WarningForm(formdata=request.form, obj=warning)
if form.validate_on_submit():
if warning is None:
warning = ContentWarning(form.title.data, form.description.data)
db.session.add(warning)
else:
form.populate_obj(warning)
db.session.commit()
return redirect(url_for("admin.warning_list"))
return render_template("admin/warnings/edit.html", warning=warning, form=form)

View File

@@ -1,21 +1,49 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
import json
from flask import Blueprint
from .support import error
bp = Blueprint("api", __name__)
from . import tokens, endpoints
@bp.errorhandler(400)
@bp.errorhandler(401)
@bp.errorhandler(403)
@bp.errorhandler(404)
def handle_exception(e):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
response = e.get_response()
# replace the body with JSON
response.data = json.dumps({
"success": False,
"code": e.code,
"name": e.name,
"description": e.description,
})
response.content_type = "application/json"
return response
@bp.route("/api/<path:path>")
def page_not_found(path):
error(404, "Endpoint or method not found")

View File

@@ -1,23 +1,27 @@
# Content DB
# ContentDB
# Copyright (C) 2019 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import request, make_response, jsonify, abort
from app.models import APIToken
from functools import wraps
from flask import request, abort
from app.models import APIToken
from .support import error
def is_api_authd(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -29,13 +33,13 @@ def is_api_authd(f):
elif value[0:7].lower() == "bearer ":
access_token = value[7:]
if len(access_token) < 10:
abort(400)
error(400, "API token is too short")
token = APIToken.query.filter_by(access_token=access_token).first()
if token is None:
abort(403)
error(403, "Unknown API token")
else:
abort(403)
error(403, "Unsupported authentication method")
return f(token=token, *args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,120 @@
from app.models import PackageRelease, db, Permission
from app.tasks.importtasks import makeVCSRelease
from celery import uuid
from flask import jsonify, make_response, url_for
import datetime
# ContentDB
# Copyright (C) 2018-21 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 Optional
from flask import jsonify, abort, make_response, url_for, current_app
from app.logic.packages import do_edit_package
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
from app.models import APIToken, Package, LuantiRelease, PackageScreenshot
def error(status, message):
return make_response(jsonify({ "success": False, "error": message }), status)
def error(code: int, msg: str):
abort(make_response(jsonify({ "success": False, "error": msg }), code))
def handleCreateRelease(token, package, title, ref):
if not token.canOperateOnPackage(package):
return error(403, "API token does not have access to the package")
# Catches LogicErrors and aborts with JSON error
def guard(f):
def ret(*args, **kwargs):
try:
return f(*args, **kwargs)
except LogicError as e:
error(e.code, e.message)
if not package.checkPerm(token.owner, Permission.MAKE_RELEASE):
return error(403, "Permission denied. Missing MAKE_RELEASE permission")
return ret
five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5)
count = package.releases.filter(PackageRelease.releaseDate > five_minutes_ago).count()
if count >= 2:
return error(429, "Too many requests, please wait before trying again")
rel = PackageRelease()
rel.package = package
rel.title = title
rel.url = ""
rel.task_id = uuid()
rel.min_rel = None
rel.max_rel = None
db.session.add(rel)
db.session.commit()
def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API"):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
makeVCSRelease.apply_async((rel.id, ref), task_id=rel.task_id)
reason += ", token=" + token.name
rel = guard(do_create_vcs_release)(token.owner, package, name, title, release_notes, ref, min_v, max_v, reason)
return jsonify({
"success": True,
"task": url_for("tasks.check", id=rel.task_id),
"release": rel.getAsDictionary()
"release": rel.as_dict()
})
def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API", commit_hash: str = None):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
reason += ", token=" + token.name
rel = guard(do_create_zip_release)(token.owner, package, name, title, release_notes, file, min_v, max_v, reason, commit_hash)
return jsonify({
"success": True,
"task": url_for("tasks.check", id=rel.task_id),
"release": rel.as_dict()
})
def api_create_screenshot(token: APIToken, package: Package, title: str, file, is_cover_image: bool, reason="API"):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
reason += ", token=" + token.name
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, is_cover_image, reason)
return jsonify({
"success": True,
"screenshot": ss.as_dict()
})
def api_order_screenshots(token: APIToken, package: Package, order: [any]):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
guard(do_order_screenshots)(token.owner, package, order)
return jsonify({
"success": True
})
def api_set_cover_image(token: APIToken, package: Package, cover_image):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
guard(do_set_cover_image)(token.owner, package, cover_image)
return jsonify({
"success": True
})
def api_edit_package(token: APIToken, package: Package, data: dict, reason: str = "API"):
if not token.can_operate_on_package(package):
error(403, "API token does not have access to the package")
reason += ", token=" + token.name
was_modified = guard(do_edit_package)(token.owner, package, False, False, data, reason)
return jsonify({
"success": True,
"package": package.as_dict(current_app.config["BASE_URL"]),
"was_modified": was_modified,
})

View File

@@ -1,37 +1,45 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import render_template, redirect, request, session, url_for, abort
from flask_user import login_required, current_user
from . import bp
from app.models import db, User, APIToken, Package, Permission
from app.utils import randomString
from app.querybuilder import QueryBuilder
from flask_babel import lazy_gettext
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired, Length
from wtforms_sqlalchemy.fields import QuerySelectField
from app.models import db, User, APIToken, Permission
from app.utils import random_string
from . import bp
from ..users.settings import get_setting_tabs
class CreateAPIToken(FlaskForm):
name = StringField("Name", [InputRequired(), Length(1, 30)])
package = QuerySelectField("Limit to package", allow_blank=True, \
name = StringField(lazy_gettext("Name"), [InputRequired(), Length(1, 30)])
package = QuerySelectField(lazy_gettext("Limit to package"), allow_blank=True,
get_pk=lambda a: a.id, get_label=lambda a: a.title)
submit = SubmitField("Save")
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/user/tokens/")
@login_required
def list_tokens_redirect():
return redirect(url_for("api.list_tokens", username=current_user.username))
@bp.route("/users/<username>/tokens/")
@@ -41,21 +49,18 @@ def list_tokens(username):
if user is None:
abort(404)
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
if not user.check_perm(current_user, Permission.CREATE_TOKEN):
abort(403)
return render_template("api/list_tokens.html", user=user)
return render_template("api/list_tokens.html", user=user, tabs=get_setting_tabs(user), current_tab="api_tokens")
@bp.route("/users/<username>/tokens/new/", methods=["GET", "POST"])
@bp.route("/users/<username>/tokens/<int:id>/edit/", methods=["GET", "POST"])
@login_required
def create_edit_token(username, id=None):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
user = User.query.filter_by(username=username).one_or_404()
if not user.check_perm(current_user, Permission.CREATE_TOKEN):
abort(403)
is_new = id is None
@@ -64,25 +69,23 @@ def create_edit_token(username, id=None):
access_token = None
if not is_new:
token = APIToken.query.get(id)
if token is None:
if token is None or token.owner != user:
abort(404)
elif token.owner != user:
abort(403)
access_token = session.pop("token_" + str(token.id), None)
form = CreateAPIToken(formdata=request.form, obj=token)
form.package.query_factory = lambda: Package.query.filter_by(author=user).all()
form.package.query_factory = lambda: user.maintained_packages.all()
if request.method == "POST" and form.validate():
if form.validate_on_submit():
if is_new:
token = APIToken()
db.session.add(token)
token.owner = user
token.access_token = randomString(32)
token.access_token = random_string(32)
form.populate_obj(token)
db.session.add(token)
db.session.commit() # save
db.session.commit()
if is_new:
# Store token so it can be shown in the edit page
@@ -100,7 +103,7 @@ def reset_token(username, id):
if user is None:
abort(404)
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
if not user.check_perm(current_user, Permission.CREATE_TOKEN):
abort(403)
token = APIToken.query.get(id)
@@ -109,7 +112,7 @@ def reset_token(username, id):
elif token.owner != user:
abort(403)
token.access_token = randomString(32)
token.access_token = random_string(32)
db.session.commit() # save
@@ -126,11 +129,9 @@ def delete_token(username, id):
if user is None:
abort(404)
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
if not user.check_perm(current_user, Permission.CREATE_TOKEN):
abort(403)
is_new = id is None
token = APIToken.query.get(id)
if token is None:
abort(404)

View File

@@ -0,0 +1,384 @@
# ContentDB
# Copyright (C) 2023 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
import typing
from flask import Blueprint, request, redirect, render_template, flash, abort, url_for, jsonify
from flask_babel import lazy_gettext, gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField, FieldList, HiddenField, TextAreaField
from wtforms.validators import InputRequired, Length, Optional, Regexp
from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity
from app.utils import nonempty_or_none, normalize_line_endings, should_return_json
from app.utils.models import is_package_page, add_audit_log, create_session
bp = Blueprint("collections", __name__)
regex_invalid_chars = re.compile("[^a-z0-9_]")
@bp.route("/collections/")
@bp.route("/collections/<author>/")
def list_all(author=None):
if author:
user = User.query.filter_by(username=author).one_or_404()
query = user.collections
else:
user = None
query = Collection.query.filter(Collection.items.any()).order_by(db.asc(Collection.title))
if "package" in request.args:
package = Package.get_by_key(request.args["package"])
if package is None:
abort(404)
query = query.filter(Collection.packages.contains(package))
collections = [x for x in query.all() if x.check_perm(current_user, Permission.VIEW_COLLECTION)]
return render_template("collections/list.html",
user=user, collections=collections,
noindex=len(collections) == 0)
@bp.route("/collections/<author>/<name>/")
def view(author, name):
collection = Collection.query \
.filter(Collection.name == name, Collection.author.has(username=author)) \
.one_or_404()
if not collection.check_perm(current_user, Permission.VIEW_COLLECTION):
abort(404)
items = collection.items
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
items = [x for x in items if x.package.check_perm(current_user, Permission.VIEW_PACKAGE)]
if should_return_json():
return jsonify([ item.package.as_key_dict() for item in items ])
else:
return render_template("collections/view.html", collection=collection, items=items)
class CollectionForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3, 100)])
name = StringField("URL", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0,
"Lower case letters (a-z), digits (0-9), and underscores (_) only")])
short_description = StringField(lazy_gettext("Short Description"), [Optional(), Length(0, 200)])
long_description = TextAreaField(lazy_gettext("Page Content"), [Optional()], filters=[nonempty_or_none, normalize_line_endings])
private = BooleanField(lazy_gettext("Private"))
pinned = BooleanField(lazy_gettext("Pinned to my profile"))
descriptions = FieldList(
StringField(lazy_gettext("Short Description"), [Optional(), Length(0, 500)], filters=[nonempty_or_none]),
min_entries=0)
package_ids = FieldList(HiddenField(), min_entries=0)
package_removed = FieldList(HiddenField(), min_entries=0)
order = HiddenField()
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/collections/new/", methods=["GET", "POST"])
@bp.route("/collections/<author>/<name>/edit/", methods=["GET", "POST"])
@login_required
def create_edit(author=None, name=None):
collection: typing.Optional[Collection] = None
if author is not None and name is not None:
collection = Collection.query \
.filter(Collection.name == name, Collection.author.has(username=author)) \
.one_or_404()
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
abort(403)
elif "author" in request.args:
author = request.args["author"]
if author != current_user.username and not current_user.rank.at_least(UserRank.EDITOR):
abort(403)
if author is None:
author = current_user
else:
author = User.query.filter_by(username=author).one()
form = CollectionForm(formdata=request.form, obj=collection)
initial_packages = []
if "package" in request.args:
for package_id in request.args.getlist("package"):
package = Package.get_by_key(package_id)
if package:
initial_packages.append(package)
if request.method == "GET":
# HACK: fix bug in wtforms
form.private.data = collection.private if collection else False
form.pinned.data = collection.pinned if collection else False
if collection:
for item in collection.items:
form.descriptions.append_entry(item.description)
form.package_ids.append_entry(item.package.get_id())
form.package_removed.append_entry("0")
else:
form.name = None
form.pinned = None
if form.validate_on_submit():
ret = handle_create_edit(collection, form, initial_packages, author)
if ret:
return ret
return render_template("collections/create_edit.html",
collection=collection, form=form)
def handle_create_edit(collection: Collection, form: CollectionForm,
initial_packages: typing.List[Package], author: User):
severity = AuditSeverity.NORMAL if author == current_user else AuditSeverity.EDITOR
name = form.name.data if collection else regex_invalid_chars.sub("", form.title.data.lower().replace(" ", "_"))
if collection is None or name != collection.name:
if Collection.query \
.filter(Collection.name == name, Collection.author == author) \
.count() > 0:
flash(gettext("A collection with a similar title already exists"), "danger")
return
if Package.query \
.filter(Package.name == name, Package.author == author) \
.count() > 0:
flash(gettext("Unable to create collection as a package with that name already exists"), "danger")
return
if collection is None:
collection = Collection()
collection.author = author
form.populate_obj(collection)
collection.name = name
db.session.add(collection)
for package in initial_packages:
link = CollectionPackage()
link.package = package
link.collection = collection
link.order = len(collection.items)
db.session.add(link)
add_audit_log(severity, current_user,
f"Created collection {collection.author.username}/{collection.name}",
collection.get_url("collections.view"), None)
else:
form.populate_obj(collection)
collection.name = name
link_lookup = {}
for link in collection.items:
link_lookup[link.package.get_id()] = link
for i, package_id in enumerate(form.package_ids):
link = link_lookup.get(package_id.data)
to_delete = form.package_removed[i].data == "1"
if link is None:
if to_delete:
continue
package = Package.get_by_key(package_id.data)
if package is None:
abort(400)
link = CollectionPackage()
link.package = package
link.collection = collection
link.description = form.descriptions[i].data
link_lookup[link.package.get_id()] = link
db.session.add(link)
elif to_delete:
db.session.delete(link)
else:
link.description = form.descriptions[i].data
for i, package_id in enumerate(form.order.data.split(",")):
if package_id != "":
link_lookup[package_id].order = i + 1
add_audit_log(severity, current_user,
f"Edited collection {collection.author.username}/{collection.name}",
collection.get_url("collections.view"), None)
db.session.commit()
return redirect(collection.get_url("collections.view"))
@bp.route("/collections/<author>/<name>/delete/", methods=["GET", "POST"])
@login_required
def delete(author, name):
collection = Collection.query \
.filter(Collection.name == name, Collection.author.has(username=author)) \
.one_or_404()
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
abort(403)
if request.method == "POST":
add_audit_log(AuditSeverity.NORMAL, current_user,
f"Deleted collection {collection.author.username}/{collection.name}",
collection.get_url("collections.view"), None)
db.session.delete(collection)
db.session.commit()
return redirect(url_for("collections.list_all", author=author))
return render_template("collections/delete.html", collection=collection)
def toggle_package(collection: Collection, package: Package):
severity = AuditSeverity.NORMAL if collection.author == current_user else AuditSeverity.EDITOR
author = User.query.get(collection.author_id) if collection.author is None else collection.author
if package in collection.packages:
CollectionPackage.query \
.filter(CollectionPackage.collection == collection, CollectionPackage.package == package) \
.delete(synchronize_session=False)
add_audit_log(severity, current_user,
f"Removed {package.get_id()} from collection {author.username}/{collection.name}",
collection.get_url("collections.view"), None)
db.session.commit()
return False
else:
link = CollectionPackage()
link.package = package
link.collection = collection
link.order = len(collection.items)
db.session.add(link)
add_audit_log(severity, current_user,
f"Added {package.get_id()} to collection {author.username}/{collection.name}",
collection.get_url("collections.view"), None)
db.session.commit()
return True
def get_or_create_favorites(session):
collection = Collection.query.filter(Collection.name == "favorites", Collection.author == current_user).first()
if collection is None:
is_new = True
collection = Collection()
collection.title = "Favorites"
collection.name = "favorites"
collection.short_description = "My favorites"
collection.author_id = current_user.id
session.add(collection)
else:
is_new = False
return collection, is_new
@bp.route("/packages/<author>/<name>/add-to/", methods=["GET", "POST"])
@is_package_page
@login_required
def package_add(package):
with create_session() as new_session:
collection, is_new = get_or_create_favorites(new_session)
if is_new:
new_session.commit()
if request.method == "POST":
collection_id = request.form["collection"]
collection = Collection.query.get(collection_id)
if collection is None:
abort(404)
if not collection.check_perm(current_user, Permission.EDIT_COLLECTION):
abort(403)
if toggle_package(collection, package):
flash(gettext("Added package to collection"), "success")
else:
flash(gettext("Removed package from collection"), "success")
return redirect(package.get_url("collections.package_add"))
collections = current_user.collections.all()
if current_user.rank.at_least(UserRank.EDITOR) and current_user.username != "ContentDB":
collections.extend(Collection.query.filter(Collection.author.has(username="ContentDB")).all())
return render_template("collections/package_add_to.html", package=package, collections=collections)
@bp.route("/packages/<author>/<name>/favorite/", methods=["POST"])
@is_package_page
@login_required
def package_toggle_favorite(package):
collection, _is_new = get_or_create_favorites(db.session)
collection.author = current_user
if toggle_package(collection, package):
msg = gettext("Added package to favorites collection")
if not collection.private:
msg += " " + gettext("(Public, change from Profile > My Collections)")
flash(msg, "success")
else:
flash(gettext("Removed package from favorites collection"), "success")
return redirect(package.get_url("packages.view"))
@bp.route("/collections/<author>/<name>/clone/", methods=["POST"])
@login_required
def clone(author, name):
old_collection: typing.Optional[Collection] = Collection.query \
.filter(Collection.name == name, Collection.author.has(username=author)) \
.one_or_404()
index = 0
new_name = name
new_title = old_collection.title
while True:
if Collection.query \
.filter(Collection.name == new_name, Collection.author == current_user) \
.count() == 0:
break
index += 1
new_name = f"{name}_{index}"
new_title = f"{old_collection.title} ({index})"
collection = Collection()
collection.title = new_title
collection.author = current_user
collection.short_description = old_collection.short_description
collection.name = new_name
collection.private = True
db.session.add(collection)
for item in old_collection.items:
new_item = CollectionPackage()
new_item.package = item.package
new_item.collection = collection
new_item.description = item.description
new_item.order = item.order
db.session.add(new_item)
add_audit_log(AuditSeverity.NORMAL, current_user,
f"Created collection {collection.name} from {old_collection.author.username}/{old_collection.name} ",
collection.get_url("collections.view"), None)
db.session.commit()
return redirect(collection.get_url("collections.view"))

View File

@@ -0,0 +1,49 @@
# ContentDB
# Copyright (C) 2023 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 flask import Blueprint, render_template
from flask_login import current_user
from sqlalchemy import or_, and_
from app.models import User, Package, PackageState, db, License, PackageReview, Collection
bp = Blueprint("donate", __name__)
@bp.route("/donate/")
def donate():
reviewed_packages = None
if current_user.is_authenticated:
reviewed_packages = Package.query.filter(
Package.state == PackageState.APPROVED,
or_(Package.reviews.any(and_(PackageReview.author_id == current_user.id, PackageReview.rating >= 3)),
Package.collections.any(and_(Collection.author_id == current_user.id, Collection.name == "favorites"))),
or_(Package.donate_url.isnot(None), Package.author.has(User.donate_url.isnot(None)))
).order_by(db.asc(Package.title)).all()
query = Package.query.filter(
Package.license.has(License.is_foss == True),
Package.media_license.has(License.is_foss == True),
Package.state == PackageState.APPROVED,
or_(Package.donate_url.isnot(None), Package.author.has(User.donate_url.isnot(None)))
).order_by(db.desc(Package.score))
packages_count = query.count()
top_packages = query.limit(40).all()
return render_template("donate/index.html",
reviewed_packages=reviewed_packages, top_packages=top_packages, packages_count=packages_count)

View File

@@ -0,0 +1,179 @@
# ContentDB
# Copyright (C) 2024 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 flask import Blueprint, jsonify, render_template, make_response
from flask_babel import gettext
from app.markdown import render_markdown
from app.models import Package, PackageState, db, PackageRelease
from app.utils import is_package_page, abs_url_for, cached, cors_allowed
bp = Blueprint("feeds", __name__)
def _make_feed(title: str, feed_url: str, items: list):
return {
"version": "https://jsonfeed.org/version/1",
"title": title,
"description": gettext("Welcome to the best place to find Luanti mods, games, and texture packs"),
"home_page_url": "https://content.luanti.org/",
"feed_url": feed_url,
"icon": "https://content.luanti.org/favicon-128.png",
"expired": False,
"items": items,
}
def _render_link(url: str):
return f"<p><a href='{url}'>Read more</a></p>"
def _get_new_packages_feed(feed_url: str) -> dict:
packages = (Package.query
.filter(Package.state == PackageState.APPROVED)
.order_by(db.desc(Package.approved_at))
.limit(100)
.all())
items = [{
"id": package.get_url("packages.view", absolute=True),
"language": "en",
"title": f"New: {package.title}",
"content_html": render_markdown(package.desc) \
if package.desc else _render_link(package.get_url("packages.view", absolute=True)),
"author": {
"name": package.author.display_name,
"avatar": package.author.get_profile_pic_url(absolute=True),
"url": abs_url_for("users.profile", username=package.author.username),
},
"image": package.get_thumb_url(level=4, abs=True, format="png"),
"url": package.get_url("packages.view", absolute=True),
"summary": package.short_desc,
"date_published": package.approved_at.isoformat(timespec="seconds") + "Z",
"tags": ["new_package"],
} for package in packages]
return _make_feed(gettext("ContentDB new packages"), feed_url, items)
def _get_releases_feed(query, feed_url: str):
releases = (query
.filter(PackageRelease.package.has(state=PackageState.APPROVED), PackageRelease.approved==True)
.order_by(db.desc(PackageRelease.created_at))
.limit(250)
.all())
items = [{
"id": release.package.get_url("packages.view_release", id=release.id, absolute=True),
"language": "en",
"title": f"\"{release.package.title}\" updated: {release.title}",
"content_html": render_markdown(release.release_notes) \
if release.release_notes else _render_link(release.package.get_url("packages.view_release", id=release.id, absolute=True)),
"author": {
"name": release.package.author.display_name,
"avatar": release.package.author.get_profile_pic_url(absolute=True),
"url": abs_url_for("users.profile", username=release.package.author.username),
},
"url": release.package.get_url("packages.view_release", id=release.id, absolute=True),
"image": release.package.get_thumb_url(level=4, abs=True, format="png"),
"summary": release.summary,
"date_published": release.created_at.isoformat(timespec="seconds") + "Z",
"tags": ["release"],
} for release in releases]
return _make_feed(gettext("ContentDB package updates"), feed_url, items)
def _get_all_feed(feed_url: str):
releases = _get_releases_feed(PackageRelease.query, "")["items"]
packages = _get_new_packages_feed("")["items"]
items = releases + packages
items.sort(reverse=True, key=lambda x: x["date_published"])
return _make_feed(gettext("ContentDB all"), feed_url, items)
def _atomify(feed):
resp = make_response(render_template("feeds/json_to_atom.xml", feed=feed))
resp.headers["Content-type"] = "application/atom+xml; charset=utf-8"
return resp
@bp.route("/feeds/all.json")
@cors_allowed
@cached(1800)
def all_json():
feed = _get_all_feed(abs_url_for("feeds.all_json"))
return jsonify(feed)
@bp.route("/feeds/all.atom")
@cors_allowed
@cached(1800)
def all_atom():
feed = _get_all_feed(abs_url_for("feeds.all_atom"))
return _atomify(feed)
@bp.route("/feeds/packages.json")
@cors_allowed
@cached(1800)
def packages_all_json():
feed = _get_new_packages_feed(abs_url_for("feeds.packages_all_json"))
return jsonify(feed)
@bp.route("/feeds/packages.atom")
@cors_allowed
@cached(1800)
def packages_all_atom():
feed = _get_new_packages_feed(abs_url_for("feeds.packages_all_atom"))
return _atomify(feed)
@bp.route("/feeds/releases.json")
@cors_allowed
@cached(1800)
def releases_all_json():
feed = _get_releases_feed(PackageRelease.query, abs_url_for("feeds.releases_all_json"))
return jsonify(feed)
@bp.route("/feeds/releases.atom")
@cors_allowed
@cached(1800)
def releases_all_atom():
feed = _get_releases_feed(PackageRelease.query, abs_url_for("feeds.releases_all_atom"))
return _atomify(feed)
@bp.route("/packages/<author>/<name>/releases_feed.json")
@cors_allowed
@is_package_page
@cached(1800)
def releases_package_json(package: Package):
feed = _get_releases_feed(package.releases, package.get_url("feeds.releases_package_json", absolute=True))
return jsonify(feed)
@bp.route("/packages/<author>/<name>/releases_feed.atom")
@cors_allowed
@is_package_page
@cached(1800)
def releases_package_atom(package: Package):
feed = _get_releases_feed(package.releases, package.get_url("feeds.releases_package_atom", absolute=True))
return _atomify(feed)

View File

@@ -1,273 +0,0 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Blueprint
bp = Blueprint("github", __name__)
from flask import redirect, url_for, request, flash, abort, render_template, jsonify, current_app
from flask_user import current_user, login_required
from sqlalchemy import func, or_, and_
from flask_github import GitHub
from app import github, csrf
from app.models import db, User, APIToken, Package, Permission
from app.utils import loginUser, randomString, abs_url_for
from app.blueprints.api.support import error, handleCreateRelease
import hmac, requests, json
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField
@bp.route("/github/start/")
def start():
return github.authorize("", redirect_uri=abs_url_for("github.callback"))
@bp.route("/github/view/")
def view_permissions():
url = "https://github.com/settings/connections/applications/" + \
current_app.config["GITHUB_CLIENT_ID"]
return redirect(url)
@bp.route("/github/callback/")
@github.authorized_handler
def callback(oauth_token):
next_url = request.args.get("next")
if oauth_token is None:
flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
return redirect(url_for("user.login"))
# Get Github username
url = "https://api.github.com/user"
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
username = r.json()["login"]
# Get user by github username
userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first()
# If logged in, connect
if current_user and current_user.is_authenticated:
if userByGithub is None:
current_user.github_username = username
db.session.commit()
flash("Linked github to account", "success")
return redirect(url_for("homepage.home"))
else:
flash("Github account is already associated with another user", "danger")
return redirect(url_for("homepage.home"))
# If not logged in, log in
else:
if userByGithub is None:
flash("Unable to find an account for that Github user", "danger")
return redirect(url_for("users.claim"))
elif loginUser(userByGithub):
if not current_user.hasPassword():
return redirect(next_url or url_for("users.set_password", optional=True))
else:
return redirect(next_url or url_for("homepage.home"))
else:
flash("Authorization failed [err=gh-login-failed]", "danger")
return redirect(url_for("user.login"))
@bp.route("/github/webhook/", methods=["POST"])
@csrf.exempt
def webhook():
json = request.json
# Get package
github_url = "github.com/" + json["repository"]["full_name"]
package = Package.query.filter(Package.repo.like("%{}%".format(github_url))).first()
if package is None:
return error(400, "Could not find package, did you set the VCS repo in CDB correctly?")
# Get all tokens for package
tokens_query = APIToken.query.filter(or_(APIToken.package==package,
and_(APIToken.package==None, APIToken.owner==package.author)))
possible_tokens = tokens_query.all()
actual_token = None
#
# Check signature
#
header_signature = request.headers.get('X-Hub-Signature')
if header_signature is None:
return error(403, "Expected payload signature")
sha_name, signature = header_signature.split('=')
if sha_name != 'sha1':
return error(403, "Expected SHA1 payload signature")
for token in possible_tokens:
mac = hmac.new(token.access_token.encode("utf-8"), msg=request.data, digestmod='sha1')
if hmac.compare_digest(str(mac.hexdigest()), signature):
actual_token = token
break
if actual_token is None:
return error(403, "Invalid authentication, couldn't validate API token")
if not package.checkPerm(actual_token.owner, Permission.APPROVE_RELEASE):
return error(403, "Only trusted members can use webhooks")
#
# Check event
#
event = request.headers.get("X-GitHub-Event")
if event == "push":
ref = json["after"]
title = json["head_commit"]["message"].partition("\n")[0]
elif event == "create" and json["ref_type"] == "tag":
ref = json["ref"]
title = ref
elif event == "ping":
return jsonify({ "success": True, "message": "Ping successful" })
else:
return error(400, "Unsupported event. Only 'push', `create:tag`, and 'ping' are supported.")
#
# Perform release
#
return handleCreateRelease(actual_token, package, title, ref)
class SetupWebhookForm(FlaskForm):
event = SelectField("Event Type", choices=[('create', 'New tag or GitHub release'), ('push', 'Push')])
submit = SubmitField("Save")
@bp.route("/github/callback/webhook/")
@github.authorized_handler
def callback_webhook(oauth_token=None):
pid = request.args.get("pid")
if pid is None:
abort(404)
current_user.github_access_token = oauth_token
db.session.commit()
return redirect(url_for("github.setup_webhook", pid=pid))
@bp.route("/github/webhook/new/", methods=["GET", "POST"])
@login_required
def setup_webhook():
pid = request.args.get("pid")
if pid is None:
abort(404)
package = Package.query.get(pid)
if package is None:
abort(404)
if not package.checkPerm(current_user, Permission.APPROVE_RELEASE):
flash("Only trusted members can use webhooks", "danger")
return redirect(package.getDetailsURL())
gh_user, gh_repo = package.getGitHubFullName()
if gh_user is None or gh_repo is None:
flash("Unable to get Github full name from repo address", "danger")
return redirect(package.getDetailsURL())
if current_user.github_access_token is None:
return github.authorize("write:repo_hook", \
redirect_uri=abs_url_for("github.callback_webhook", pid=pid))
form = SetupWebhookForm(formdata=request.form)
if request.method == "POST" and form.validate():
token = APIToken()
token.name = "GitHub Webhook for " + package.title
token.owner = current_user
token.access_token = randomString(32)
token.package = package
event = form.event.data
if event != "push" and event != "create":
abort(500)
if handleMakeWebhook(gh_user, gh_repo, package, \
current_user.github_access_token, event, token):
flash("Successfully created webhook", "success")
return redirect(package.getDetailsURL())
else:
return redirect(url_for("github.setup_webhook", pid=package.id))
return render_template("github/setup_webhook.html", \
form=form, package=package)
def handleMakeWebhook(gh_user, gh_repo, package, oauth, event, token):
url = "https://api.github.com/repos/{}/{}/hooks".format(gh_user, gh_repo)
headers = {
"Authorization": "token " + oauth
}
data = {
"name": "web",
"active": True,
"events": [event],
"config": {
"url": abs_url_for("github.webhook"),
"content_type": "json",
"secret": token.access_token
},
}
# First check that the webhook doesn't already exist
r = requests.get(url, headers=headers)
if r.status_code == 401 or r.status_code == 403:
current_user.github_access_token = None
db.session.commit()
return False
if r.status_code != 200:
flash("Failed to create webhook, received response from Github " +
str(r.status_code) + ": " +
str(r.json().get("message")), "danger")
return False
for hook in r.json():
if hook.get("config") and hook["config"].get("url") and \
hook["config"]["url"] == data["config"]["url"]:
flash("Failed to create webhook, as it already exists", "danger")
return False
# Create it
r = requests.post(url, headers=headers, data=json.dumps(data))
if r.status_code == 201:
db.session.add(token)
db.session.commit()
return True
elif r.status_code == 401 or r.status_code == 403:
current_user.github_access_token = None
db.session.commit()
return False
else:
flash("Failed to create webhook, received response from Github " +
str(r.status_code) + ": " +
str(r.json().get("message")), "danger")
return False

View File

@@ -1,67 +0,0 @@
# ContentDB
# Copyright (C) 2020 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Blueprint, request
bp = Blueprint("gitlab", __name__)
from app import csrf
from app.models import Package, APIToken, Permission
from app.blueprints.api.support import error, handleCreateRelease
@bp.route("/gitlab/webhook/", methods=["POST"])
@csrf.exempt
def webhook():
json = request.json
# Get package
gitlab_url = "gitlab.com/{}/{}".format(json["project"]["namespace"], json["project"]["name"])
package = Package.query.filter(Package.repo.like("%{}%".format(gitlab_url))).first()
if package is None:
return error(400, "Unknown package")
# Get all tokens for package
secret = request.headers.get("X-Gitlab-Token")
if secret is None:
return error(403, "Token required")
token = APIToken.query.filter_by(access_token=secret).first()
if secret is None:
return error(403, "Invalid authentication")
if not package.checkPerm(token.owner, Permission.APPROVE_RELEASE):
return error(403, "Only trusted members can use webhooks")
#
# Check event
#
event = json["event_name"]
if event == "push":
ref = json["after"]
title = ref[:5]
elif event == "tag_push":
ref = json["ref"]
title = ref.replace("refs/tags/", "")
else:
return error(400, "Unsupported event. Only 'push' and 'tag_push' are supported.")
#
# Perform release
#
return handleCreateRelease(token, package, title, ref)

View File

@@ -1,21 +1,136 @@
from flask import Blueprint, render_template
# ContentDB
# Copyright (C) 2018-23 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 flask import Blueprint, render_template, redirect
from sqlalchemy import and_
from app.models import Package, PackageReview, Thread, User, PackageState, db, PackageType, PackageRelease, Tags, Tag, \
Collection, License, Language
bp = Blueprint("homepage", __name__)
from app.models import *
import flask_menu as menu
from sqlalchemy.orm import joinedload, subqueryload, load_only, noload
from sqlalchemy.sql.expression import func
PKGS_PER_ROW = 4
# GAMEJAM_BANNER = "https://jam.luanti.org/img/banner.png"
#
# class GameJam:
# cover_image = type("", (), dict(url=GAMEJAM_BANNER))()
# tags = []
#
# def get_cover_image_url(self):
# return GAMEJAM_BANNER
#
# def get_url(self, _name):
# return "/gamejam/"
#
# title = "Luanti Game Jam 2023: \"Unexpected\""
# author = None
#
# short_desc = "The game jam has finished! It's now up to the community to play and rate the games."
# type = type("", (), dict(value="Competition"))()
# content_warnings = []
# reviews = []
@bp.route("/gamejam/")
def gamejam():
return redirect("https://jam.luanti.org/")
@bp.route("/")
@menu.register_menu(bp, ".", "Home")
def home():
query = Package.query.filter_by(approved=True, soft_deleted=False)
count = query.count()
new = query.order_by(db.desc(Package.created_at)).limit(8).all()
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
downloads_result = db.session.query(func.sum(PackageRelease.downloads)).one_or_none()
def package_load(query):
return query.options(
load_only(Package.name, Package.title, Package.short_desc, Package.state, raiseload=True),
subqueryload(Package.main_screenshot),
joinedload(Package.author).load_only(User.username, User.display_name, raiseload=True),
joinedload(Package.license).load_only(License.name, License.is_foss, raiseload=True),
joinedload(Package.media_license).load_only(License.name, License.is_foss, raiseload=True))
def package_spotlight_load(query):
return query.options(
load_only(Package.name, Package.title, Package.type, Package.short_desc, Package.state, Package.cover_image_id, raiseload=True),
subqueryload(Package.main_screenshot),
joinedload(Package.tags),
joinedload(Package.content_warnings),
joinedload(Package.author).load_only(User.username, User.display_name, raiseload=True),
subqueryload(Package.cover_image),
joinedload(Package.license).load_only(License.name, License.is_foss, raiseload=True),
joinedload(Package.media_license).load_only(License.name, License.is_foss, raiseload=True))
def review_load(query):
return query.options(
load_only(PackageReview.id, PackageReview.rating, PackageReview.created_at, PackageReview.language_id, raiseload=True),
joinedload(PackageReview.author).load_only(User.username, User.rank, User.email, User.display_name, User.profile_pic, User.is_active, raiseload=True),
joinedload(PackageReview.votes),
joinedload(PackageReview.language).load_only(Language.title, raiseload=True),
joinedload(PackageReview.thread).load_only(Thread.title, Thread.replies_count, raiseload=True).subqueryload(Thread.first_reply),
joinedload(PackageReview.package)
.load_only(Package.title, Package.name, raiseload=True)
.joinedload(Package.author).load_only(User.username, User.display_name, raiseload=True))
query = Package.query.filter_by(state=PackageState.APPROVED)
count = db.session.query(Package.id).filter(Package.state == PackageState.APPROVED).count()
spotlight_pkgs = package_spotlight_load(query.filter(
Package.collections.any(and_(Collection.name == "spotlight", Collection.author.has(username="ContentDB"))))
.order_by(func.random())).limit(6).all()
# spotlight_pkgs.insert(0, GameJam())
new = package_load(query).order_by(db.desc(Package.approved_at)).limit(PKGS_PER_ROW).all() # 0.06
pop_mod = package_load(query).filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(2*PKGS_PER_ROW).all()
pop_gam = package_load(query).filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(2*PKGS_PER_ROW).all()
pop_txp = package_load(query).filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(2*PKGS_PER_ROW).all()
high_reviewed = package_load(query.order_by(db.desc(Package.score - Package.score_downloads))
.filter(Package.reviews.any()).limit(PKGS_PER_ROW)).all()
recent_releases_query = (
db.session.query(
Package.id,
func.max(PackageRelease.created_at).label("max_created_at")
)
.join(PackageRelease, Package.releases)
.group_by(Package.id)
.order_by(db.desc("max_created_at"))
.limit(3*PKGS_PER_ROW)
.subquery())
updated = (
package_load(db.session.query(Package)
.select_from(recent_releases_query)
.join(Package, Package.id == recent_releases_query.c.id)
.filter(Package.state == PackageState.APPROVED)
.limit(PKGS_PER_ROW))
.all())
reviews = review_load(PackageReview.query.filter(PackageReview.rating > 3)
.order_by(db.desc(PackageReview.created_at))).limit(5).all()
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
return render_template("index.html", count=count, downloads=downloads, \
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
tags = db.session.query(func.count(Tags.c.tag_id), Tag) \
.select_from(Tag).outerjoin(Tags).join(Package).filter(Package.state == PackageState.APPROVED)\
.group_by(Tag.id).order_by(db.asc(Tag.title)).all()
return render_template("index.html", count=count, downloads=downloads, tags=tags, spotlight_pkgs=spotlight_pkgs,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed,
reviews=reviews)

View File

@@ -1,36 +0,0 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
bp = Blueprint("metapackages", __name__)
from flask_user import *
from app.models import *
@bp.route("/metapackages/")
def list_all():
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
return render_template("meta/list.html", mpackages=mpackages)
@bp.route("/metapackages/<name>/")
def view(name):
mpackage = MetaPackage.query.filter_by(name=name).first()
if mpackage is None:
abort(404)
return render_template("meta/view.html", mpackage=mpackage)

View File

@@ -0,0 +1,126 @@
# ContentDB
# Copyright (C) 2020 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 datetime
from flask import Blueprint, make_response
from sqlalchemy import or_, and_
from sqlalchemy.sql.expression import func
from app.models import Package, db, User, UserRank, PackageState, PackageReview, ThreadReply, Collection, AuditLogEntry, \
PackageTranslation, Language
from app.rediscache import get_key
bp = Blueprint("metrics", __name__)
def generate_metrics():
def write_single_stat(name, help, type, value):
fmt = "# HELP {name} {help}\n# TYPE {name} {type}\n{name} {value}\n\n"
return fmt.format(name=name, help=help, type=type, value=value)
def gen_labels(labels):
pieces = [f"{key}=\"{val}\"" for key, val in labels.items()]
return ",".join(pieces)
def write_array_stat(name, help, type, data):
result = "# HELP {name} {help}\n# TYPE {name} {type}\n" \
.format(name=name, help=help, type=type)
for entry in data:
assert(len(entry) == 2)
result += "{name}{{{labels}}} {value}\n" \
.format(name=name, labels=gen_labels(entry[0]), value=entry[1])
return result + "\n"
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
packages = Package.query.filter_by(state=PackageState.APPROVED).count()
users = User.query.filter(User.rank > UserRank.NOT_JOINED, User.rank != UserRank.BOT, User.is_active).count()
authors = User.query.filter(User.packages.any(state=PackageState.APPROVED)).count()
one_day_ago = datetime.datetime.now() - datetime.timedelta(days=1)
one_week_ago = datetime.datetime.now() - datetime.timedelta(weeks=1)
one_month_ago = datetime.datetime.now() - datetime.timedelta(weeks=4)
active_users_day = User.query.filter(and_(User.rank != UserRank.BOT, or_(
User.audit_log_entries.any(AuditLogEntry.created_at > one_day_ago),
User.replies.any(ThreadReply.created_at > one_day_ago)))).count()
active_users_week = User.query.filter(and_(User.rank != UserRank.BOT, or_(
User.audit_log_entries.any(AuditLogEntry.created_at > one_week_ago),
User.replies.any(ThreadReply.created_at > one_week_ago)))).count()
active_users_month = User.query.filter(and_(User.rank != UserRank.BOT, or_(
User.audit_log_entries.any(AuditLogEntry.created_at > one_month_ago),
User.replies.any(ThreadReply.created_at > one_month_ago)))).count()
reviews = PackageReview.query.count()
comments = ThreadReply.query.count()
collections = Collection.query.count()
score_result = db.session.query(func.sum(Package.score)).one_or_none()
score = 0 if not score_result or not score_result[0] else score_result[0]
packages_with_translations = (db.session.query(PackageTranslation.package_id)
.filter(PackageTranslation.language_id != "en")
.group_by(PackageTranslation.package_id).count())
packages_with_translations_meta = (db.session.query(PackageTranslation.package_id)
.filter(PackageTranslation.short_desc.is_not(None), PackageTranslation.language_id != "en")
.group_by(PackageTranslation.package_id).count())
languages_packages = (db.session.query(PackageTranslation.language_id, func.count(Package.id))
.select_from(PackageTranslation).outerjoin(Package)
.order_by(db.asc(PackageTranslation.language_id))
.group_by(PackageTranslation.language_id).all())
languages_packages_meta = (db.session.query(PackageTranslation.language_id, func.count(Package.id))
.select_from(PackageTranslation).outerjoin(Package)
.filter(PackageTranslation.short_desc.is_not(None))
.order_by(db.asc(PackageTranslation.language_id))
.group_by(PackageTranslation.language_id).all())
ret = ""
ret += write_single_stat("contentdb_packages", "Total packages", "gauge", packages)
ret += write_single_stat("contentdb_users", "Number of registered users", "gauge", users)
ret += write_single_stat("contentdb_authors", "Number of users with packages", "gauge", authors)
ret += write_single_stat("contentdb_users_active_1d", "Number of daily active registered users", "gauge", active_users_day)
ret += write_single_stat("contentdb_users_active_1w", "Number of weekly active registered users", "gauge", active_users_week)
ret += write_single_stat("contentdb_users_active_1m", "Number of monthly active registered users", "gauge", active_users_month)
ret += write_single_stat("contentdb_downloads", "Total downloads", "gauge", downloads)
ret += write_single_stat("contentdb_emails", "Number of emails sent", "counter", int(get_key("emails_sent", "0")))
ret += write_single_stat("contentdb_reviews", "Number of reviews", "gauge", reviews)
ret += write_single_stat("contentdb_comments", "Number of comments", "gauge", comments)
ret += write_single_stat("contentdb_collections", "Number of collections", "gauge", collections)
ret += write_single_stat("contentdb_score", "Total package score", "gauge", score)
ret += write_single_stat("contentdb_packages_with_translations", "Number of packages with translations", "gauge",
packages_with_translations)
ret += write_single_stat("contentdb_packages_with_translations_meta", "Number of packages with translated meta",
"gauge", packages_with_translations_meta)
ret += write_array_stat("contentdb_languages_translated",
"Number of packages per language", "gauge",
[({"language": x[0]}, x[1]) for x in languages_packages])
ret += write_array_stat("contentdb_languages_translated_meta",
"Number of packages with translated short desc per language", "gauge",
[({"language": x[0]}, x[1]) for x in languages_packages_meta])
return ret
@bp.route("/metrics")
def metrics():
response = make_response(generate_metrics(), 200)
response.mimetype = "text/plain"
return response

View File

@@ -0,0 +1,68 @@
# ContentDB
# Copyright (C) 2018-21 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 flask import Blueprint, redirect, render_template, abort
from sqlalchemy import func
from app.models import MetaPackage, Package, db, Dependency, PackageState, ForumTopic
bp = Blueprint("modnames", __name__)
@bp.route("/metapackages/<path:path>")
def mp_redirect(path):
return redirect("/modnames/" + path)
@bp.route("/modnames/")
def list_all():
modnames = db.session.query(MetaPackage, func.count(Package.id)) \
.select_from(MetaPackage).outerjoin(MetaPackage.packages) \
.order_by(db.asc(MetaPackage.name)) \
.group_by(MetaPackage.id).all()
return render_template("modnames/list.html", modnames=modnames)
@bp.route("/modnames/<name>/")
def view(name):
modname = MetaPackage.query.filter_by(name=name).first()
if modname is None:
abort(404)
dependers = db.session.query(Package) \
.select_from(MetaPackage) \
.filter(MetaPackage.name==name) \
.join(MetaPackage.dependencies) \
.join(Dependency.depender) \
.filter(Dependency.optional==False, Package.state==PackageState.APPROVED) \
.all()
optional_dependers = db.session.query(Package) \
.select_from(MetaPackage) \
.filter(MetaPackage.name==name) \
.join(MetaPackage.dependencies) \
.join(Dependency.depender) \
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
.all()
similar_topics = ForumTopic.query \
.filter_by(name=name) \
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
return render_template("modnames/view.html", modname=modname,
dependers=dependers, optional_dependers=optional_dependers,
similar_topics=similar_topics)

View File

@@ -1,34 +1,48 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import Blueprint, render_template, redirect, url_for
from flask_user import current_user, login_required
from app.models import db
from flask_login import current_user, login_required
from sqlalchemy import or_, desc
from app.models import db, Notification, NotificationType
bp = Blueprint("notifications", __name__)
@bp.route("/notifications/")
@login_required
def list_all():
return render_template("notifications/list.html")
notifications = Notification.query.filter(Notification.user == current_user,
Notification.type != NotificationType.EDITOR_ALERT, Notification.type != NotificationType.EDITOR_MISC) \
.order_by(desc(Notification.created_at)) \
.all()
editor_notifications = Notification.query.filter(Notification.user == current_user,
or_(Notification.type == NotificationType.EDITOR_ALERT, Notification.type == NotificationType.EDITOR_MISC)) \
.order_by(desc(Notification.created_at)) \
.all()
return render_template("notifications/list.html",
notifications=notifications, editor_notifications=editor_notifications)
@bp.route("/notifications/clear/", methods=["POST"])
@login_required
def clear():
current_user.notifications.clear()
Notification.query.filter_by(user=current_user).delete()
db.session.commit()
return redirect(url_for("notifications.list_all"))

View File

@@ -0,0 +1,264 @@
# ContentDB
# Copyright (C) 2023 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 urllib.parse as urlparse
from urllib.parse import urlencode
import typing
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, abort, make_response, flash
from flask_babel import lazy_gettext, gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, URLField, SelectField
from wtforms.validators import InputRequired, Length, Optional
from app import csrf
from app.blueprints.users.settings import get_setting_tabs
from app.models import db, OAuthClient, User, Permission, APIToken, AuditSeverity, UserRank
from app.utils import random_string, add_audit_log
bp = Blueprint("oauth", __name__)
def build_redirect_url(url: str, code: str, state: typing.Optional[str]):
params = {"code": code}
if state is not None:
params["state"] = state
url_parts = list(urlparse.urlparse(url))
query = dict(urlparse.parse_qsl(url_parts[4]))
query.update(params)
url_parts[4] = urlencode(query)
return urlparse.urlunparse(url_parts)
@bp.route("/oauth/authorize/", methods=["GET", "POST"])
@login_required
def oauth_start():
response_type = request.args.get("response_type", "code")
if response_type != "code":
return "Unsupported response_type, only code is supported", 400
client_id = request.args.get("client_id", "")
if client_id == "":
return "Missing client_id", 400
redirect_uri = request.args.get("redirect_uri", "")
if redirect_uri == "":
return "Missing redirect_uri", 400
client = OAuthClient.query.get_or_404(client_id)
if client.redirect_url != redirect_uri:
return "redirect_uri does not match client", 400
if not client.approved and client.owner != current_user:
abort(404)
scope = request.args.get("scope", "public")
if scope != "public":
return "Unsupported scope, only public is supported", 400
state = request.args.get("state")
token = APIToken.query.filter(APIToken.client == client, APIToken.owner == current_user).first()
if token:
token.access_token = random_string(32)
token.auth_code = random_string(32)
db.session.commit()
return redirect(build_redirect_url(client.redirect_url, token.auth_code, state))
if request.method == "POST":
action = request.form["action"]
if action == "cancel":
return redirect(client.redirect_url)
elif action == "authorize":
token = APIToken()
token.access_token = random_string(32)
token.name = f"Token for {client.title} by {client.owner.username}"
token.owner = current_user
token.client = client
assert client is not None
token.auth_code = random_string(32)
db.session.add(token)
add_audit_log(AuditSeverity.USER, current_user,
f"Granted \"{scope}\" to OAuth2 application \"{client.title}\" by {client.owner.username} [{client_id}] ",
url_for("users.profile", username=current_user.username))
db.session.commit()
return redirect(build_redirect_url(client.redirect_url, token.auth_code, state))
return render_template("oauth/authorize.html", client=client)
def error(code: int, msg: str):
abort(make_response(jsonify({"success": False, "error": msg}), code))
@bp.route("/oauth/token/", methods=["POST"])
@csrf.exempt
def oauth_grant():
form = request.form
grant_type = request.args.get("grant_type", "authorization_code")
if grant_type != "authorization_code":
error(400, "Unsupported grant_type, only authorization_code is supported")
client_id = form.get("client_id", "")
if client_id == "":
error(400, "Missing client_id")
client_secret = form.get("client_secret", "")
if client_secret == "":
error(400, "Missing client_secret")
code = form.get("code", "")
if code == "":
error(400, "Missing code")
client = OAuthClient.query.filter_by(id=client_id, secret=client_secret).first()
if client is None:
error(400, "client_id and/or client_secret is incorrect")
token = APIToken.query.filter_by(auth_code=code).first()
if token is None or token.client != client:
error(400, "Incorrect code. It may have already been redeemed")
token.auth_code = None
db.session.commit()
return jsonify({
"success": True,
"access_token": token.access_token,
"token_type": "Bearer",
})
@bp.route("/user/apps/")
@login_required
def list_clients_redirect():
return redirect(url_for("oauth.list_clients", username=current_user.username))
@bp.route("/users/<username>/apps/")
@login_required
def list_clients(username):
user = User.query.filter_by(username=username).first_or_404()
if not user.check_perm(current_user, Permission.CREATE_OAUTH_CLIENT):
abort(403)
return render_template("oauth/list_clients.html", user=user, tabs=get_setting_tabs(user), current_tab="oauth_clients")
class OAuthClientForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(5, 30)])
description = StringField(lazy_gettext("Description"), [Optional()])
redirect_url = URLField(lazy_gettext("Redirect URL"), [InputRequired(), Length(5, 123)])
app_type = SelectField(lazy_gettext("App Type"), [InputRequired()], choices=[
("server", "Server-side (client_secret is kept safe)"),
("client", "Client-side (client_secret is visible to all users)"),
], coerce=lambda x: x)
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/users/<username>/apps/new/", methods=["GET", "POST"])
@bp.route("/users/<username>/apps/<id_>/edit/", methods=["GET", "POST"])
@login_required
def create_edit_client(username, id_=None):
user = User.query.filter_by(username=username).first_or_404()
if not user.check_perm(current_user, Permission.CREATE_OAUTH_CLIENT):
abort(403)
is_new = id_ is None
client = None
if id_ is not None:
client = OAuthClient.query.get_or_404(id_)
if client.owner != user:
abort(404)
form = OAuthClientForm(formdata=request.form, obj=client)
if form.validate_on_submit():
if is_new:
if OAuthClient.query.filter(OAuthClient.title.ilike(form.title.data.strip())).count() > 0:
flash(gettext("An OAuth client with that title already exists. Please choose a new title."), "danger")
return render_template("oauth/create_edit.html", user=user, form=form, client=client)
client = OAuthClient()
db.session.add(client)
client.owner = user
client.id = random_string(24)
client.secret = random_string(32)
client.approved = current_user.rank.at_least(UserRank.EDITOR)
form.populate_obj(client)
verb = "Created" if is_new else "Edited"
add_audit_log(AuditSeverity.NORMAL, current_user,
f"{verb} OAuth2 application {client.title} by {client.owner.username} [{client.id}]",
url_for("oauth.create_edit_client", username=client.owner.username, id_=client.id))
db.session.commit()
return redirect(url_for("oauth.create_edit_client", username=username, id_=client.id))
return render_template("oauth/create_edit.html", user=user, form=form, client=client)
@bp.route("/users/<username>/apps/<id_>/delete/", methods=["POST"])
@login_required
def delete_client(username, id_):
user = User.query.filter_by(username=username).first_or_404()
if not user.check_perm(current_user, Permission.CREATE_OAUTH_CLIENT):
abort(403)
client = OAuthClient.query.get(id_)
if client is None or client.owner != user:
abort(404)
add_audit_log(AuditSeverity.NORMAL, current_user,
f"Deleted OAuth2 application {client.title} by {client.owner.username} [{client.id}]",
url_for("users.profile", username=current_user.username))
db.session.delete(client)
db.session.commit()
return redirect(url_for("oauth.list_clients", username=username))
@bp.route("/users/<username>/apps/<id_>/revoke-all/", methods=["POST"])
@login_required
def revoke_all(username, id_):
user = User.query.filter_by(username=username).first_or_404()
if not user.check_perm(current_user, Permission.CREATE_OAUTH_CLIENT):
abort(403)
client = OAuthClient.query.get(id_)
if client is None or client.owner != user:
abort(404)
add_audit_log(AuditSeverity.NORMAL, current_user,
f"Revoked all user tokens for OAuth2 application {client.title} by {client.owner.username} [{client.id}]",
url_for("oauth.create_edit_client", username=client.owner.username, id_=client.id))
client.tokens = []
db.session.commit()
flash(gettext("Revoked all user tokens"), "success")
return redirect(url_for("oauth.create_edit_client", username=client.owner.username, id_=client.id))

View File

@@ -1,21 +1,87 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import Blueprint
from flask_babel import gettext
from app.models import User, Package, Permission, PackageType
bp = Blueprint("packages", __name__)
from . import packages, screenshots, releases
def get_package_tabs(user: User, package: Package):
if package is None or not package.check_perm(user, Permission.EDIT_PACKAGE):
return []
retval = [
{
"id": "edit",
"title": gettext("Edit Details"),
"url": package.get_url("packages.create_edit")
},
{
"id": "translation",
"title": gettext("Translation"),
"url": package.get_url("packages.translation")
},
{
"id": "releases",
"title": gettext("Releases"),
"url": package.get_url("packages.list_releases")
},
{
"id": "screenshots",
"title": gettext("Screenshots"),
"url": package.get_url("packages.screenshots")
},
{
"id": "maintainers",
"title": gettext("Maintainers"),
"url": package.get_url("packages.edit_maintainers")
},
{
"id": "audit",
"title": gettext("Audit Log"),
"url": package.get_url("packages.audit")
},
{
"id": "stats",
"title": gettext("Statistics"),
"url": package.get_url("packages.statistics")
},
{
"id": "share",
"title": gettext("Share and Badges"),
"url": package.get_url("packages.share")
},
{
"id": "remove",
"title": gettext("Remove / Unpublish"),
"url": package.get_url("packages.remove")
}
]
if package.type == PackageType.MOD or package.type == PackageType.TXP:
retval.insert(2, {
"id": "game_support",
"title": gettext("Supported Games"),
"url": package.get_url("packages.game_support")
})
return retval
from . import packages, advanced_search, screenshots, releases, reviews, game_hub

View File

@@ -0,0 +1,103 @@
# ContentDB
# Copyright (C) 2024 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 flask import render_template
from flask_babel import lazy_gettext, gettext
from flask_wtf import FlaskForm
from wtforms.fields.choices import SelectField, SelectMultipleField
from wtforms.fields.simple import StringField, BooleanField
from wtforms.validators import Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
from . import bp
from ...models import PackageType, Tag, db, ContentWarning, License, Language, LuantiRelease, Package, PackageState
def make_label(obj: Tag | ContentWarning):
translated = obj.get_translated()
if translated["description"]:
return "{}: {}".format(translated["title"], translated["description"])
else:
return translated["title"]
def get_hide_choices():
ret = [
("android_default", gettext("Android Default")),
("desktop_default", gettext("Desktop Default")),
("nonfree", gettext("Non-free")),
("wip", gettext("Work in Progress")),
("deprecated", gettext("Deprecated")),
("*", gettext("All content warnings")),
]
content_warnings = ContentWarning.query.order_by(db.asc(ContentWarning.name)).all()
tags = Tag.query.order_by(db.asc(Tag.name)).all()
ret += [(x.name, make_label(x)) for x in content_warnings + tags]
return ret
class AdvancedSearchForm(FlaskForm):
q = StringField(lazy_gettext("Query"), [Optional()])
type = SelectMultipleField(lazy_gettext("Type"), [Optional()],
choices=PackageType.choices(), coerce=PackageType.coerce)
author = StringField(lazy_gettext("Author"), [Optional()])
tag = QuerySelectMultipleField(lazy_gettext('Tags'),
query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)),
get_pk=lambda a: a.name, get_label=make_label)
flag = QuerySelectMultipleField(lazy_gettext('Content Warnings'),
query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)),
get_pk=lambda a: a.name, get_label=make_label)
license = QuerySelectMultipleField(lazy_gettext("License"), [Optional()],
query_factory=lambda: License.query.order_by(db.asc(License.name)),
allow_blank=True, blank_value="",
get_pk=lambda a: a.name, get_label=lambda a: a.name)
game = QuerySelectField(lazy_gettext("Supports Game"), [Optional()],
query_factory=lambda: Package.query.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED).order_by(db.asc(Package.name)),
allow_blank=True, blank_value="",
get_pk=lambda a: f"{a.author.username}/{a.name}",
get_label=lambda a: lazy_gettext("%(title)s by %(author)s", title=a.title, author=a.author.display_name))
lang = QuerySelectField(lazy_gettext("Language"),
query_factory=lambda: Language.query.order_by(db.asc(Language.title)),
allow_blank=True, blank_value="",
get_pk=lambda a: a.id, get_label=lambda a: a.title)
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
query_factory=lambda: LuantiRelease.query.order_by(db.asc(LuantiRelease.id)),
allow_blank=True, blank_value="",
get_pk=lambda a: a.value, get_label=lambda a: a.name)
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[
("", ""),
("name", lazy_gettext("Name")),
("title", lazy_gettext("Title")),
("score", lazy_gettext("Package score")),
("reviews", lazy_gettext("Reviews")),
("downloads", lazy_gettext("Downloads")),
("created_at", lazy_gettext("Created At")),
("approved_at", lazy_gettext("Approved At")),
("last_release", lazy_gettext("Last Release")),
])
order = SelectField(lazy_gettext("Order"), [Optional()], choices=[
("desc", lazy_gettext("Descending")),
("asc", lazy_gettext("Ascending")),
])
random = BooleanField(lazy_gettext("Random order"))
@bp.route("/packages/advanced-search/")
def advanced_search():
form = AdvancedSearchForm()
form.hide.choices = get_hide_choices()
return render_template("packages/advanced_search.html", form=form)

View File

@@ -1,173 +0,0 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from flask_user import *
from app import app
from app.models import *
from app.utils import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from . import PackageForm
class EditRequestForm(PackageForm):
edit_title = StringField("Edit Title", [InputRequired(), Length(1, 100)])
edit_desc = TextField("Edit Description", [Optional()])
@app.route("/packages/<author>/<name>/requests/new/", methods=["GET","POST"])
@app.route("/packages/<author>/<name>/requests/<id>/edit/", methods=["GET","POST"])
@login_required
@is_package_page
def create_edit_editrequest_page(package, id=None):
edited_package = package
erequest = None
if id is not None:
erequest = EditRequest.query.get(id)
if erequest.package != package:
abort(404)
if not erequest.checkPerm(current_user, Permission.EDIT_EDITREQUEST):
abort(403)
if erequest.status != 0:
flash("Can't edit EditRequest, it has already been merged or rejected", "danger")
return redirect(erequest.getURL())
edited_package = Package(package)
erequest.applyAll(edited_package)
form = EditRequestForm(request.form, obj=edited_package)
if request.method == "GET":
deps = edited_package.dependencies
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
form.provides_str.data = MetaPackage.ListToSpec(edited_package.provides)
if request.method == "POST" and form.validate():
if erequest is None:
erequest = EditRequest()
erequest.package = package
erequest.author = current_user
erequest.title = form["edit_title"].data
erequest.desc = form["edit_desc"].data
db.session.add(erequest)
EditRequestChange.query.filter_by(request=erequest).delete()
wasChangeMade = False
for e in PackagePropertyKey:
newValue = form[e.name].data
oldValue = getattr(package, e.name)
newValueComp = newValue
oldValueComp = oldValue
if type(newValue) is str:
newValue = newValue.replace("\r\n", "\n")
newValueComp = newValue.strip()
oldValueComp = "" if oldValue is None else oldValue.strip()
if newValueComp != oldValueComp:
change = EditRequestChange()
change.request = erequest
change.key = e
change.oldValue = e.convert(oldValue)
change.newValue = e.convert(newValue)
db.session.add(change)
wasChangeMade = True
if wasChangeMade:
msg = "{}: Edit request #{} {}" \
.format(package.title, erequest.id, "created" if id is None else "edited")
triggerNotif(package.author, current_user, msg, erequest.getURL())
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
db.session.commit()
return redirect(erequest.getURL())
else:
flash("No changes detected", "warning")
elif erequest is not None:
form["edit_title"].data = erequest.title
form["edit_desc"].data = erequest.desc
return render_template("packages/editrequest_create_edit.html", package=package, form=form)
@app.route("/packages/<author>/<name>/requests/<id>/")
@is_package_page
def view_editrequest_page(package, id):
erequest = EditRequest.query.get(id)
if erequest is None or erequest.package != package:
abort(404)
clearNotifications(erequest.getURL())
return render_template("packages/editrequest_view.html", package=package, request=erequest)
@app.route("/packages/<author>/<name>/requests/<id>/approve/", methods=["POST"])
@is_package_page
def approve_editrequest_page(package, id):
if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
flash("You don't have permission to do that.", "danger")
return redirect(package.getDetailsURL())
erequest = EditRequest.query.get(id)
if erequest is None or erequest.package != package:
abort(404)
if erequest.status != 0:
flash("Edit request has already been resolved", "danger")
else:
erequest.status = 1
erequest.applyAll(package)
msg = "{}: Edit request #{} merged".format(package.title, erequest.id)
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
triggerNotif(package.author, current_user, msg, erequest.getURL())
db.session.commit()
return redirect(package.getDetailsURL())
@app.route("/packages/<author>/<name>/requests/<id>/reject/", methods=["POST"])
@is_package_page
def reject_editrequest_page(package, id):
if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
flash("You don't have permission to do that.", "danger")
return redirect(package.getDetailsURL())
erequest = EditRequest.query.get(id)
if erequest is None or erequest.package != package:
abort(404)
if erequest.status != 0:
flash("Edit request has already been resolved", "danger")
else:
erequest.status = 2
msg = "{}: Edit request #{} rejected".format(package.title, erequest.id)
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
triggerNotif(package.author, current_user, msg, erequest.getURL())
db.session.commit()
return redirect(package.getDetailsURL())

View File

@@ -0,0 +1,53 @@
# ContentDB
# Copyright (C) 2022 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 flask import render_template, abort
from sqlalchemy.orm import joinedload
from . import bp
from app.utils import is_package_page
from app.models import Package, PackageType, PackageState, db, PackageRelease
@bp.route("/packages/<author>/<name>/hub/")
@is_package_page
def game_hub(package: Package):
if package.type != PackageType.GAME:
abort(404)
def join(query):
return query.options(
joinedload(Package.license),
joinedload(Package.media_license))
query = Package.query.filter(Package.supported_games.any(game=package, supports=True), Package.state==PackageState.APPROVED)
count = query.count()
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(8).all()
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all()
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
.filter(Package.supported_games.any(game=package, supports=True), Package.state==PackageState.APPROVED) \
.order_by(db.desc(PackageRelease.created_at)) \
.limit(20).all()
updated = updated[:4]
return render_template("packages/game_hub.html", package=package, count=count,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp,
high_reviewed=high_reviewed)

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +1,124 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
import os
from flask import *
from flask_user import *
from . import bp
from app.rediscache import has_key, set_key, make_download_key
from app.models import *
from app.tasks.importtasks import makeVCSRelease, checkZipRelease
from app.utils import *
from celery import uuid
from flask import render_template, request, redirect, flash, url_for, abort
from flask_babel import lazy_gettext, gettext
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms import StringField, SubmitField, BooleanField, RadioField, FileField
from wtforms.fields.simple import TextAreaField
from wtforms.validators import InputRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, LuantiRelease, \
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
from app.rediscache import has_key, set_temp_key, make_download_key
from app.tasks.importtasks import check_update_config
from app.utils import is_user_bot, is_package_page, nonempty_or_none, normalize_line_endings
from . import bp, get_package_tabs
@bp.route("/packages/<author>/<name>/releases/", methods=["GET", "POST"])
@is_package_page
def list_releases(package):
return render_template("packages/releases_list.html",
package=package,
tabs=get_package_tabs(current_user, package), current_tab="releases")
def get_mt_releases(is_max):
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
query = LuantiRelease.query.order_by(db.asc(LuantiRelease.id))
if is_max:
query = query.limit(query.count() - 1)
else:
query = query.filter(MinetestRelease.name != "0.4.17")
query = query.filter(LuantiRelease.name != "0.4.17")
return query
class CreatePackageReleaseForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 30)])
uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
vcsLabel = StringField("VCS Commit Hash, Branch, or Tag", default="master")
fileUpload = FileField("File Upload")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
name = StringField(lazy_gettext("Name"), [InputRequired(), Length(1, 30)])
title = StringField(lazy_gettext("Title"), [Optional(), Length(1, 100)], filters=[nonempty_or_none])
release_notes = TextAreaField(lazy_gettext("Release Notes"), [Optional(), Length(1, 5000)],
filters=[nonempty_or_none, normalize_line_endings])
upload_mode = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload")
vcs_label = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None)
file_upload = FileField(lazy_gettext("File Upload"))
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField("Save")
submit = SubmitField(lazy_gettext("Save"))
class EditPackageReleaseForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 30)])
url = StringField("URL", [URL])
task_id = StringField("Task ID", filters = [lambda x: x or None])
approved = BooleanField("Is Approved")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
name = StringField(lazy_gettext("Name"), [InputRequired(), Length(1, 30)])
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(1, 30)], filters=[nonempty_or_none])
release_notes = TextAreaField(lazy_gettext("Release Notes"), [Optional(), Length(1, 5000)],
filters=[nonempty_or_none, normalize_line_endings])
url = StringField(lazy_gettext("URL"), [Optional()])
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
approved = BooleanField(lazy_gettext("Is Approved"))
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField("Save")
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
@login_required
@is_package_page
def create_release(package):
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
return redirect(package.getDetailsURL())
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
return redirect(url_for("users.email_notifications"))
if not package.check_perm(current_user, Permission.MAKE_RELEASE):
return redirect(package.get_url("packages.view"))
# Initial form class from post data and default data
form = CreatePackageReleaseForm()
if package.repo is not None:
form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
if request.method != "POST":
form["uploadOpt"].data = "vcs"
form.upload_mode.choices = [("vcs", gettext("Import from Git")), ("upload", gettext("Upload .zip file"))]
if request.method == "GET":
form.upload_mode.data = "vcs"
form.vcs_label.data = request.args.get("ref")
if request.method == "POST" and form.validate():
if form["uploadOpt"].data == "vcs":
rel = PackageRelease()
rel.package = package
rel.title = form["title"].data
rel.url = ""
rel.task_id = uuid()
rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual()
db.session.add(rel)
db.session.commit()
if request.method == "GET":
form.title.data = request.args.get("title")
makeVCSRelease.apply_async((rel.id, form["vcsLabel"].data), task_id=rel.task_id)
msg = "{}: Release {} created".format(package.title, rel.title)
triggerNotif(package.author, current_user, msg, rel.getEditURL())
db.session.commit()
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
else:
uploadedUrl, uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
if uploadedUrl is not None:
rel = PackageRelease()
rel.package = package
rel.title = form["title"].data
rel.url = uploadedUrl
rel.task_id = uuid()
rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual()
db.session.add(rel)
db.session.commit()
checkZipRelease.apply_async((rel.id, uploadedPath), task_id=rel.task_id)
msg = "{}: Release {} created".format(package.title, rel.title)
triggerNotif(package.author, current_user, msg, rel.getEditURL())
db.session.commit()
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
if form.validate_on_submit():
try:
if form.upload_mode.data == "vcs":
rel = do_create_vcs_release(current_user, package, form.name.data, form.title.data, form.release_notes.data,
form.vcs_label.data, form.min_rel.data.get_actual(), form.max_rel.data.get_actual())
else:
rel = do_create_zip_release(current_user, package, form.name.data, form.title.data, form.release_notes.data,
form.file_upload.data, form.min_rel.data.get_actual(), form.max_rel.data.get_actual())
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.get_edit_url()))
except LogicError as e:
flash(e.message, "danger")
return render_template("packages/release_new.html", package=package, form=form)
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
@bp.route("/packages/<author>/<name>/releases/<int:id>/download/")
@is_package_page
def download_release(package, id):
release = PackageRelease.query.get(id)
@@ -128,114 +126,133 @@ def download_release(package, id):
abort(404)
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
if ip is not None:
if ip is not None and not is_user_bot():
user_agent = request.headers.get("User-Agent") or ""
is_luanti = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
reason = request.args.get("reason")
PackageDailyStats.update(package, is_luanti, reason)
key = make_download_key(ip, release.package)
if not has_key(key):
set_key(key, "true")
set_temp_key(key, "true")
bonus = 1
if not package.getIsFOSS():
bonus *= 0.1
bonus = 0
if reason == "new":
bonus = 1
elif reason == "dependency" or reason == "update":
bonus = 0.5
PackageRelease.query.filter_by(id=release.id).update({
"downloads": PackageRelease.downloads + 1
})
Package.query.filter_by(id=package.id).update({
"downloads": Package.downloads + 1,
"score_downloads": Package.score_downloads + bonus,
"score": Package.score + bonus
})
db.session.commit()
db.session.commit()
return redirect(release.url, code=300)
return redirect(release.url)
@bp.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
@login_required
@bp.route("/packages/<author>/<name>/releases/<int:id>/")
@is_package_page
def edit_release(package, id):
release = PackageRelease.query.get(id)
def view_release(package, id):
release: PackageRelease = PackageRelease.query.get(id)
if release is None or release.package != package:
abort(404)
clearNotifications(release.getEditURL())
return render_template("packages/release_view.html", package=package, release=release)
canEdit = package.checkPerm(current_user, Permission.MAKE_RELEASE)
canApprove = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
@bp.route("/packages/<author>/<name>/releases/<int:id>/edit/", methods=["GET", "POST"])
@login_required
@is_package_page
def edit_release(package, id):
release: PackageRelease = PackageRelease.query.get(id)
if release is None or release.package != package:
abort(404)
canEdit = package.check_perm(current_user, Permission.MAKE_RELEASE)
canApprove = release.check_perm(current_user, Permission.APPROVE_RELEASE)
if not (canEdit or canApprove):
return redirect(package.getDetailsURL())
return redirect(package.get_url("packages.view"))
# Initial form class from post data and default data
form = EditPackageReleaseForm(formdata=request.form, obj=release)
# HACK: fix bug in wtforms
if request.method == "GET":
# HACK: fix bug in wtforms
form.approved.data = release.approved
if request.method == "POST" and form.validate():
wasApproved = release.approved
if form.validate_on_submit():
if canEdit:
release.title = form["title"].data
release.min_rel = form["min_rel"].data.getActual()
release.max_rel = form["max_rel"].data.getActual()
release.name = form.name.data
release.title = form.title.data
release.release_notes = form.release_notes.data
release.min_rel = form.min_rel.data.get_actual()
release.max_rel = form.max_rel.data.get_actual()
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
release.url = form["url"].data
release.task_id = form["task_id"].data
if package.check_perm(current_user, Permission.CHANGE_RELEASE_URL):
release.url = form.url.data
release.task_id = form.task_id.data
if release.task_id is not None:
release.task_id = None
if canApprove:
release.approved = form["approved"].data
else:
release.approved = wasApproved
if form.approved.data:
release.approve(current_user)
elif canApprove:
release.approved = False
db.session.commit()
return redirect(package.getDetailsURL())
return redirect(package.get_url("packages.list_releases"))
return render_template("packages/release_edit.html", package=package, release=release, form=form)
class BulkReleaseForm(FlaskForm):
set_min = BooleanField("Set Min")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
set_min = BooleanField(lazy_gettext("Set Min"))
min_rel = QuerySelectField(lazy_gettext("Minimum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
set_max = BooleanField("Set Max")
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
set_max = BooleanField(lazy_gettext("Set Max"))
max_rel = QuerySelectField(lazy_gettext("Maximum Luanti Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
only_change_none = BooleanField("Only change values previously set as none")
submit = SubmitField("Update")
only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
submit = SubmitField(lazy_gettext("Update"))
@bp.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
@login_required
@is_package_page
def bulk_change_release(package):
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
return redirect(package.getDetailsURL())
if not package.check_perm(current_user, Permission.MAKE_RELEASE):
return redirect(package.get_url("packages.view"))
# Initial form class from post data and default data
form = BulkReleaseForm()
if request.method == "GET":
form.only_change_none.data = True
elif request.method == "POST" and form.validate():
elif form.validate_on_submit():
only_change_none = form.only_change_none.data
for release in package.releases.all():
if form["set_min"].data and (not only_change_none or release.min_rel is None):
release.min_rel = form["min_rel"].data.getActual()
if form["set_max"].data and (not only_change_none or release.max_rel is None):
release.max_rel = form["max_rel"].data.getActual()
if form.set_min.data and (not only_change_none or release.min_rel is None):
release.min_rel = form.min_rel.data.get_actual()
if form.set_max.data and (not only_change_none or release.max_rel is None):
release.max_rel = form.max_rel.data.get_actual()
db.session.commit()
return redirect(package.getDetailsURL())
return redirect(package.get_url("packages.list_releases"))
return render_template("packages/release_bulk_change.html", package=package, form=form)
@bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"])
@bp.route("/packages/<author>/<name>/releases/<int:id>/delete/", methods=["POST"])
@login_required
@is_package_page
def delete_release(package, id):
@@ -243,10 +260,136 @@ def delete_release(package, id):
if release is None or release.package != package:
abort(404)
if not release.checkPerm(current_user, Permission.DELETE_RELEASE):
return redirect(release.getEditURL())
if not release.check_perm(current_user, Permission.DELETE_RELEASE):
return redirect(package.get_url("packages.list_releases"))
db.session.delete(release)
db.session.commit()
return redirect(package.getDetailsURL())
if release.file_path and os.path.isfile(release.file_path):
os.remove(release.file_path)
return redirect(package.get_url("packages.view"))
class PackageUpdateConfigFrom(FlaskForm):
trigger = RadioField(lazy_gettext("Trigger"), [InputRequired()],
choices=[(PackageUpdateTrigger.COMMIT, lazy_gettext("New Commit")),
(PackageUpdateTrigger.TAG, lazy_gettext("New Tag"))],
coerce=PackageUpdateTrigger.coerce, default=PackageUpdateTrigger.TAG)
ref = StringField(lazy_gettext("Branch name"), [Optional()], default=None)
action = RadioField(lazy_gettext("Action"), [InputRequired()],
choices=[("notification", lazy_gettext("Send notification and mark as outdated")), ("make_release", lazy_gettext("Create release"))],
default="make_release")
submit = SubmitField(lazy_gettext("Save Settings"))
disable = SubmitField(lazy_gettext("Disable Automation"))
def set_update_config(package, form):
if package.update_config is None:
package.update_config = PackageUpdateConfig()
db.session.add(package.update_config)
form.populate_obj(package.update_config)
package.update_config.ref = nonempty_or_none(form.ref.data)
package.update_config.make_release = form.action.data == "make_release"
if package.update_config.trigger == PackageUpdateTrigger.COMMIT:
if package.update_config.last_commit is None:
last_release = package.releases.first()
if last_release and last_release.commit_hash:
package.update_config.last_commit = last_release.commit_hash
elif package.update_config.trigger == PackageUpdateTrigger.TAG:
# Only create releases for tags created after this
package.update_config.last_commit = None
package.update_config.last_tag = None
package.update_config.outdated_at = None
package.update_config.auto_created = False
db.session.commit()
if package.update_config.last_commit is None:
check_update_config.delay(package.id)
@bp.route("/packages/<author>/<name>/update-config/", methods=["GET", "POST"])
@login_required
@is_package_page
def update_config(package):
if not package.check_perm(current_user, Permission.MAKE_RELEASE):
abort(403)
if not package.repo:
flash(gettext("Please add a Git repository URL in order to set up automatic releases"), "danger")
return redirect(package.get_url("packages.create_edit"))
form = PackageUpdateConfigFrom(obj=package.update_config)
if request.method == "GET":
if package.update_config:
form.action.data = "make_release" if package.update_config.make_release else "notification"
elif request.args.get("action") == "notification":
form.trigger.data = PackageUpdateTrigger.COMMIT
form.action.data = "notification"
if "trigger" in request.args:
form.trigger.data = PackageUpdateTrigger.get(request.args["trigger"])
if form.validate_on_submit():
if form.disable.data:
flash(gettext("Deleted update configuration"), "success")
if package.update_config:
db.session.delete(package.update_config)
db.session.commit()
else:
set_update_config(package, form)
if not form.disable.data and package.releases.count() == 0:
flash(gettext("Now, please create an initial release"), "success")
return redirect(package.get_url("packages.create_release"))
return redirect(package.get_url("packages.list_releases"))
return render_template("packages/update_config.html", package=package, form=form)
@bp.route("/packages/<author>/<name>/setup-releases/")
@login_required
@is_package_page
def setup_releases(package):
if not package.check_perm(current_user, Permission.MAKE_RELEASE):
abort(403)
if package.update_config:
return redirect(package.get_url("packages.update_config"))
return render_template("packages/release_wizard.html", package=package)
@bp.route("/user/update-configs/")
@bp.route("/users/<username>/update-configs/", methods=["GET", "POST"])
@login_required
def bulk_update_config(username=None):
if username is None:
return redirect(url_for("packages.bulk_update_config", username=current_user.username))
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403)
form = PackageUpdateConfigFrom()
if form.validate_on_submit():
for package in user.packages.filter(Package.state != PackageState.DELETED, Package.repo.isnot(None)).all():
set_update_config(package, form)
return redirect(url_for("packages.bulk_update_config", username=username))
confs = user.packages \
.filter(Package.state != PackageState.DELETED,
Package.update_config.has()) \
.order_by(db.asc(Package.title)).all()
return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form)

View File

@@ -0,0 +1,276 @@
# ContentDB
# Copyright (C) 2020 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 collections import namedtuple
import typing
from flask import render_template, request, redirect, flash, url_for, abort, jsonify
from flask_babel import gettext, lazy_gettext, get_locale
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, RadioField
from wtforms.validators import InputRequired, Length, DataRequired
from wtforms_sqlalchemy.fields import QuerySelectField
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission, AuditSeverity, PackageState, Language
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_package_page, add_notification, get_int_or_abort, is_yes, is_safe_url, rank_required, \
add_audit_log, has_blocked_domains, should_return_json, normalize_line_endings
from . import bp
@bp.route("/reviews/")
def list_reviews():
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 100))
pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page=page, per_page=num)
return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items)
def get_default_language():
locale = get_locale()
if locale:
return Language.query.filter_by(id=locale.language).first()
return None
class ReviewForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3, 100)])
language = QuerySelectField(lazy_gettext("Language"), [DataRequired()],
allow_blank=True,
query_factory=lambda: Language.query.order_by(db.asc(Language.title)),
get_pk=lambda a: a.id,
get_label=lambda a: a.title,
default=get_default_language)
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
rating = RadioField(lazy_gettext("Rating"), [InputRequired()],
choices=[("5", lazy_gettext("Yes")), ("3", lazy_gettext("Neutral")), ("1", lazy_gettext("No"))])
btn_submit = SubmitField(lazy_gettext("Save"))
@bp.route("/packages/<author>/<name>/review/", methods=["GET", "POST"])
@login_required
@is_package_page
def review(package):
if current_user in package.maintainers:
flash(gettext("You can't review your own package!"), "danger")
return redirect(package.get_url("packages.view"))
if package.state != PackageState.APPROVED:
abort(404)
review = PackageReview.query.filter_by(package=package, author=current_user).first()
can_review = review is not None or current_user.can_review_ratelimit()
if not can_review:
flash(gettext("You've reviewed too many packages recently. Please wait before trying again, and consider making your reviews more detailed"), "danger")
form = ReviewForm(formdata=request.form, obj=review)
# Set default values
if request.method == "GET" and review:
form.title.data = review.thread.title
form.rating.data = str(review.rating)
form.comment.data = review.thread.first_reply.comment
# Validate and submit
elif can_review and form.validate_on_submit():
if has_blocked_domains(form.comment.data, current_user.username, f"review of {package.get_id()}"):
flash(gettext("Linking to blocked sites is not allowed"), "danger")
else:
was_new = False
if not review:
was_new = True
review = PackageReview()
review.package = package
review.author = current_user
db.session.add(review)
review.rating = int(form.rating.data)
review.language = form.language.data
thread = review.thread
if not thread:
thread = Thread()
thread.author = current_user
thread.private = False
thread.package = package
thread.review = review
db.session.add(thread)
thread.watchers.append(current_user)
reply = ThreadReply()
reply.thread = thread
reply.author = current_user
reply.comment = form.comment.data
db.session.add(reply)
thread.replies.append(reply)
else:
reply = thread.first_reply
reply.comment = form.comment.data
thread.title = form.title.data
db.session.commit()
package.recalculate_score()
if was_new:
notif_msg = "New review '{}'".format(form.title.data)
type = NotificationType.NEW_REVIEW
else:
notif_msg = "Updated review '{}'".format(form.title.data)
type = NotificationType.OTHER
add_notification(package.maintainers, current_user, type, notif_msg,
url_for("threads.view", id=thread.id), package)
if was_new:
msg = f"Reviewed {package.title} ({review.language.title}): {thread.get_view_url(absolute=True)}"
post_discord_webhook.delay(thread.author.display_name, msg, False)
db.session.commit()
return redirect(package.get_url("packages.view"))
return render_template("packages/review_create_edit.html",
form=form, package=package, review=review)
@bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
@login_required
@is_package_page
def delete_review(package, reviewer):
review = PackageReview.query \
.filter(PackageReview.package == package, PackageReview.author.has(username=reviewer)) \
.first()
if review is None or review.package != package:
abort(404)
if not review.check_perm(current_user, Permission.DELETE_REVIEW):
abort(403)
thread = review.thread
reply = ThreadReply()
reply.thread = thread
reply.author = current_user
reply.comment = "_converted review into a thread_"
reply.is_status_update = True
db.session.add(reply)
thread.review = None
msg = "Converted review by {} to thread".format(review.author.display_name)
add_audit_log(AuditSeverity.MODERATION if current_user.username != reviewer else AuditSeverity.NORMAL,
current_user, msg, thread.get_view_url(), thread.package)
notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title)
add_notification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package)
db.session.delete(review)
package.recalculate_score()
db.session.commit()
return redirect(thread.get_view_url())
def handle_review_vote(package: Package, review_id: int) -> typing.Optional[str]:
if current_user in package.maintainers:
return gettext("You can't vote on the reviews on your own package!")
review: PackageReview = PackageReview.query.get(review_id)
if review is None or review.package != package:
abort(404)
if review.author == current_user:
return gettext("You can't vote on your own reviews!")
is_positive = is_yes(request.form["is_positive"])
vote = PackageReviewVote.query.filter_by(review=review, user=current_user).first()
if vote is None:
vote = PackageReviewVote()
vote.review = review
vote.user = current_user
vote.is_positive = is_positive
db.session.add(vote)
elif vote.is_positive == is_positive:
db.session.delete(vote)
else:
vote.is_positive = is_positive
review.update_score()
db.session.commit()
@bp.route("/packages/<author>/<name>/review/<int:review_id>/", methods=["POST"])
@login_required
@is_package_page
def review_vote(package, review_id):
msg = handle_review_vote(package, review_id)
if should_return_json():
if msg:
return jsonify({"success": False, "error": msg}), 403
else:
return jsonify({"success": True})
if msg:
flash(msg, "danger")
next_url = request.args.get("r")
if next_url and is_safe_url(next_url):
return redirect(next_url)
else:
return redirect(review.thread.get_view_url())
@bp.route("/packages/<author>/<name>/review-votes/")
@rank_required(UserRank.ADMIN)
@is_package_page
def review_votes(package):
user_biases = {}
for review in package.reviews:
review_sign = review.as_weight()
for vote in review.votes:
user_biases[vote.user.username] = user_biases.get(vote.user.username, [0, 0])
vote_sign = 1 if vote.is_positive else -1
vote_bias = review_sign * vote_sign
if vote_bias == 1:
user_biases[vote.user.username][0] += 1
else:
user_biases[vote.user.username][1] += 1
reviews = package.reviews.all()
BiasInfo = namedtuple("BiasInfo", "username balance with_ against no_vote perc_with")
user_biases_info = []
for username, bias in user_biases.items():
total_votes = bias[0] + bias[1]
balance = bias[0] - bias[1]
perc_with = round((100 * bias[0]) / total_votes)
user_biases_info.append(BiasInfo(username, balance, bias[0], bias[1], len(reviews) - total_votes, perc_with))
user_biases_info.sort(key=lambda x: -abs(x.balance))
return render_template("packages/review_votes.html", package=package, reviews=reviews,
user_biases=user_biases_info)

View File

@@ -1,74 +1,100 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
import os
from flask import *
from flask_user import *
from . import bp
from app.models import *
from app.utils import *
from flask import render_template, request, redirect, flash, url_for, abort
from flask_babel import lazy_gettext, gettext
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from flask_wtf.file import FileRequired
from wtforms import StringField, SubmitField, BooleanField, FileField
from wtforms.validators import Length, DataRequired, Optional
from wtforms_sqlalchemy.fields import QuerySelectField
from app.logic.LogicError import LogicError
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
from . import bp, get_package_tabs
from app.models import Permission, db, PackageScreenshot
from app.utils import is_package_page
class CreateScreenshotForm(FlaskForm):
title = StringField("Title/Caption", [Optional()])
fileUpload = FileField("File Upload", [InputRequired()])
submit = SubmitField("Save")
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
file_upload = FileField(lazy_gettext("File Upload"), [FileRequired()])
submit = SubmitField(lazy_gettext("Save"))
class EditScreenshotForm(FlaskForm):
title = StringField("Title/Caption", [Optional()])
approved = BooleanField("Is Approved")
delete = BooleanField("Delete")
submit = SubmitField("Save")
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
approved = BooleanField(lazy_gettext("Is Approved"))
submit = SubmitField(lazy_gettext("Save"))
class EditPackageScreenshotsForm(FlaskForm):
cover_image = QuerySelectField(lazy_gettext("Cover Image"), [DataRequired()], allow_blank=True, get_pk=lambda a: a.id, get_label=lambda a: a.title)
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/packages/<author>/<name>/screenshots/", methods=["GET", "POST"])
@login_required
@is_package_page
def screenshots(package):
if not package.check_perm(current_user, Permission.ADD_SCREENSHOTS):
return redirect(package.get_url("packages.view"))
form = EditPackageScreenshotsForm(obj=package)
form.cover_image.query = package.screenshots
if request.method == "POST":
order = request.form.get("order")
if order:
try:
do_order_screenshots(current_user, package, order.split(","))
return redirect(package.get_url("packages.view"))
except LogicError as e:
flash(e.message, "danger")
if form.validate_on_submit():
form.populate_obj(package)
db.session.commit()
return render_template("packages/screenshots.html", package=package, form=form,
tabs=get_package_tabs(current_user, package), current_tab="screenshots")
@bp.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
@login_required
@is_package_page
def create_screenshot(package, id=None):
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
return redirect(package.getDetailsURL())
def create_screenshot(package):
if not package.check_perm(current_user, Permission.ADD_SCREENSHOTS):
return redirect(package.get_url("packages.view"))
# Initial form class from post data and default data
form = CreateScreenshotForm()
if request.method == "POST" and form.validate():
uploadedUrl, uploadedPath = doFileUpload(form.fileUpload.data, "image",
"a PNG or JPG image file")
if uploadedUrl is not None:
ss = PackageScreenshot()
ss.package = package
ss.title = form["title"].data or "Untitled"
ss.url = uploadedUrl
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
db.session.add(ss)
msg = "{}: Screenshot added {}" \
.format(package.title, ss.title)
triggerNotif(package.author, current_user, msg, package.getDetailsURL())
db.session.commit()
return redirect(package.getDetailsURL())
if form.validate_on_submit():
try:
do_create_screenshot(current_user, package, form.title.data, form.file_upload.data, False)
return redirect(package.get_url("packages.screenshots"))
except LogicError as e:
flash(e.message, "danger")
return render_template("packages/screenshot_new.html", package=package, form=form)
@bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/screenshots/<int:id>/edit/", methods=["GET", "POST"])
@login_required
@is_package_page
def edit_screenshot(package, id):
@@ -76,31 +102,49 @@ def edit_screenshot(package, id):
if screenshot is None or screenshot.package != package:
abort(404)
canEdit = package.checkPerm(current_user, Permission.ADD_SCREENSHOTS)
canApprove = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
if not (canEdit or canApprove):
return redirect(package.getDetailsURL())
clearNotifications(screenshot.getEditURL())
can_edit = package.check_perm(current_user, Permission.ADD_SCREENSHOTS)
can_approve = package.check_perm(current_user, Permission.APPROVE_SCREENSHOT)
if not (can_edit or can_approve):
return redirect(package.get_url("packages.screenshots"))
# Initial form class from post data and default data
form = EditScreenshotForm(formdata=request.form, obj=screenshot)
if request.method == "POST" and form.validate():
if canEdit and form["delete"].data:
PackageScreenshot.query.filter_by(id=id).delete()
form = EditScreenshotForm(obj=screenshot)
if form.validate_on_submit():
was_approved = screenshot.approved
if can_edit:
screenshot.title = form.title.data or "Untitled"
if can_approve:
screenshot.approved = form.approved.data
else:
wasApproved = screenshot.approved
if canEdit:
screenshot.title = form["title"].data or "Untitled"
if canApprove:
screenshot.approved = form["approved"].data
else:
screenshot.approved = wasApproved
screenshot.approved = was_approved
db.session.commit()
return redirect(package.getDetailsURL())
return redirect(package.get_url("packages.screenshots"))
return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form)
@bp.route("/packages/<author>/<name>/screenshots/<int:id>/delete/", methods=["POST"])
@login_required
@is_package_page
def delete_screenshot(package, id):
screenshot = PackageScreenshot.query.get(id)
if screenshot is None or screenshot.package != package:
abort(404)
if not package.check_perm(current_user, Permission.ADD_SCREENSHOTS):
flash(gettext("Permission denied"), "danger")
return redirect(url_for("homepage.home"))
if package.cover_image == screenshot:
package.cover_image = None
db.session.merge(package)
db.session.delete(screenshot)
db.session.commit()
os.remove(screenshot.file_path)
return redirect(package.get_url("packages.screenshots"))

View File

@@ -0,0 +1,182 @@
# ContentDB
# Copyright (C) 2022 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 flask import Blueprint, request, render_template, url_for, abort, flash
from flask_babel import lazy_gettext
from flask_login import current_user
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
from wtforms.validators import InputRequired, Length, Optional, DataRequired
from app.logic.uploads import upload_file
from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import (is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for,
random_string, add_replies)
bp = Blueprint("report", __name__)
class ReportForm(FlaskForm):
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"))
@bp.route("/report/", methods=["GET", "POST"])
def report():
is_anon = not current_user.is_authenticated or not is_no(request.args.get("anon"))
url = request.args.get("url")
if url:
if url.startswith("/report/"):
abort(404)
url = abs_url_samesite(url)
form = ReportForm() if current_user.is_authenticated else None
if form and request.method == "GET":
try:
form.category.data = ReportCategory.coerce(request.args.get("category"))
except KeyError:
pass
form.url.data = url
form.title.data = request.args.get("title", "")
if form and form.validate_on_submit():
report = Report()
report.id = random_string(8)
report.user = current_user if current_user.is_authenticated else None
form.populate_obj(report)
if current_user.is_authenticated:
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:
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
report.message = ip_addr + "\n\n" + report.message
db.session.add(report)
db.session.flush()
if form.file_upload.data:
atmt = ReportAttachment()
report.attachments.add(atmt)
uploaded_url, _ = upload_file(form.file_upload.data, "image", lazy_gettext("a PNG, JPEG, or WebP image file"))
atmt.url = uploaded_url
db.session.add(atmt)
if current_user.is_authenticated:
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
url_for("report.view", rid=report.id))
db.session.commit()
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

@@ -1,43 +1,44 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import Blueprint, jsonify, url_for, request, redirect, render_template
from flask_login import login_required, current_user
from flask import *
from flask_user import *
import flask_menu as menu
from app import csrf
from app.models import *
from app.tasks import celery, TaskError
from app.tasks.importtasks import getMeta
from app.utils import shouldReturnJson
from app.utils import *
from app.models import UserRank
from app.tasks import celery
from app.tasks.importtasks import get_meta
from app.utils import should_return_json
bp = Blueprint("tasks", __name__)
@csrf.exempt
@bp.route("/tasks/getmeta/new/", methods=["POST"])
@login_required
def start_getmeta():
from flask import request
author = request.args.get("author")
author = current_user.forums_username if author is None else author
aresult = getMeta.delay(request.args.get("url"), author)
aresult = get_meta.delay(request.args.get("url"), author)
return jsonify({
"poll_url": url_for("tasks.check", id=aresult.id),
})
@bp.route("/tasks/<id>/")
def check(id):
result = celery.AsyncResult(id)
@@ -45,17 +46,19 @@ def check(id):
traceback = result.traceback
result = result.result
info = None
if isinstance(result, Exception):
info = {
'id': id,
'status': status,
}
if current_user.is_authenticated and current_user.rank.atLeast(UserRank.ADMIN):
if current_user.is_authenticated and current_user.rank.at_least(UserRank.ADMIN):
info["error"] = str(traceback)
elif str(result)[1:12] == "TaskError: ":
info["error"] = str(result)[12:-1]
if hasattr(result, "value"):
info["error"] = result.value
else:
info["error"] = str(result)
else:
info["error"] = "Unknown server error"
else:
@@ -65,7 +68,7 @@ def check(id):
'result': result,
}
if shouldReturnJson():
if should_return_json():
return jsonify(info)
else:
r = request.args.get("r")

View File

@@ -1,137 +1,291 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import Blueprint, request, render_template, abort, flash, redirect, url_for
from flask_babel import gettext, lazy_gettext
from sqlalchemy.orm import selectinload
from flask import *
from app.markdown import get_user_mentions, render_markdown
from app.tasks.webhooktasks import post_discord_webhook
bp = Blueprint("threads", __name__)
from flask_user import *
from app.models import *
from app.utils import triggerNotif, clearNotifications
import datetime
from flask_login import current_user, login_required
from app.models import Package, db, User, Permission, Thread, UserRank, AuditSeverity, \
NotificationType, ThreadReply
from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains, \
normalize_line_endings
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length
from app.utils import get_int_or_abort
@bp.route("/threads/")
def list_all():
query = Thread.query
if not Permission.SEE_THREAD.check(current_user):
query = query.filter_by(private=False)
package = None
pid = request.args.get("pid")
if pid:
pid = get_int_or_abort(pid)
query = query.filter_by(package_id=pid)
package = Package.query.get_or_404(pid)
query = query.filter_by(package=package)
return render_template("threads/list.html", threads=query.all())
query = query.filter_by(review_id=None)
query = query.order_by(db.desc(Thread.created_at))
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 100))
pagination = query.paginate(page=page, per_page=num)
return render_template("threads/list.html", pagination=pagination, threads=pagination.items,
package=package, noindex=pid)
@bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
@login_required
def subscribe(id):
thread = Thread.query.get(id)
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
if thread is None or not thread.check_perm(current_user, Permission.SEE_THREAD):
abort(404)
if current_user in thread.watchers:
flash("Already subscribed!", "success")
flash(gettext("Already subscribed!"), "success")
else:
flash("Subscribed to thread", "success")
flash(gettext("Subscribed to thread"), "success")
thread.watchers.append(current_user)
db.session.commit()
return redirect(url_for("threads.view", id=id))
return redirect(thread.get_view_url())
@bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
@login_required
def unsubscribe(id):
thread = Thread.query.get(id)
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
if thread is None or not thread.check_perm(current_user, Permission.SEE_THREAD):
abort(404)
if current_user in thread.watchers:
flash("Unsubscribed!", "success")
flash(gettext("Unsubscribed!"), "success")
thread.watchers.remove(current_user)
db.session.commit()
else:
flash("Not subscribed to thread", "success")
flash(gettext("Already not subscribed!"), "success")
return redirect(url_for("threads.view", id=id))
return redirect(thread.get_view_url())
@bp.route("/threads/<int:id>/set-lock/", methods=["POST"])
@login_required
def set_lock(id):
thread = Thread.query.get(id)
if thread is None or not thread.check_perm(current_user, Permission.LOCK_THREAD):
abort(404)
thread.locked = is_yes(request.args.get("lock"))
if thread.locked is None:
abort(400)
if thread.locked:
msg = "Locked thread '{}'".format(thread.title)
flash(gettext("Locked thread"), "success")
else:
msg = "Unlocked thread '{}'".format(thread.title)
flash(gettext("Unlocked thread"), "success")
add_notification(thread.watchers, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package)
add_audit_log(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package)
db.session.commit()
return redirect(thread.get_view_url())
@bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
@login_required
def delete_thread(id):
thread = Thread.query.get(id)
if thread is None or not thread.check_perm(current_user, Permission.DELETE_THREAD):
abort(404)
if request.method == "GET":
return render_template("threads/delete_thread.html", thread=thread)
summary = "\n\n".join([("<{}> {}".format(reply.author.display_name, reply.comment)) for reply in thread.replies])
msg = "Deleted thread {} by {}".format(thread.title, thread.author.display_name)
db.session.delete(thread)
add_audit_log(AuditSeverity.MODERATION, current_user, msg, None, thread.package, summary)
db.session.commit()
return redirect(url_for("homepage.home"))
@bp.route("/threads/<int:id>/delete-reply/", methods=["GET", "POST"])
@login_required
def delete_reply(id):
thread = Thread.query.get(id)
if thread is None:
abort(404)
reply_id = request.args.get("reply")
if reply_id is None:
abort(404)
reply = ThreadReply.query.get(reply_id)
if reply is None or reply.thread != thread:
abort(404)
if thread.first_reply == reply:
flash(gettext("Cannot delete thread opening post!"), "danger")
return redirect(thread.get_view_url())
if not reply.check_perm(current_user, Permission.DELETE_REPLY):
abort(403)
if request.method == "GET":
return render_template("threads/delete_reply.html", thread=thread, reply=reply)
msg = "Deleted reply by {}".format(reply.author.display_name)
add_audit_log(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package, reply.comment)
db.session.delete(reply)
db.session.commit()
return redirect(thread.get_view_url())
class CommentForm(FlaskForm):
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(2, 2000)], filters=[normalize_line_endings])
btn_submit = SubmitField(lazy_gettext("Comment"))
@bp.route("/threads/<int:id>/edit/", methods=["GET", "POST"])
@login_required
def edit_reply(id):
thread = Thread.query.get(id)
if thread is None:
abort(404)
reply_id = request.args.get("reply")
if reply_id is None:
abort(404)
reply: ThreadReply = ThreadReply.query.get(reply_id)
if reply is None or reply.thread != thread:
abort(404)
if not reply.check_perm(current_user, Permission.EDIT_REPLY):
abort(403)
form = CommentForm(formdata=request.form, obj=reply)
if form.validate_on_submit():
comment = form.comment.data
if has_blocked_domains(comment, current_user.username, f"edit to reply {reply.get_url(True)}"):
flash(gettext("Linking to blocked sites is not allowed"), "danger")
else:
msg = "Edited reply by {}".format(reply.author.display_name)
severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION
add_notification(reply.author, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package)
add_audit_log(severity, current_user, msg, thread.get_view_url(), thread.package, reply.comment)
reply.comment = comment
db.session.commit()
return redirect(thread.get_view_url())
return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form)
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
def view(id):
clearNotifications(url_for("threads.view", id=id))
thread = Thread.query.get(id)
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
thread: Thread = Thread.query.get(id)
if thread is None or not thread.check_perm(current_user, Permission.SEE_THREAD):
abort(404)
if current_user.is_authenticated and request.method == "POST":
comment = request.form["comment"]
form = CommentForm(formdata=request.form) if thread.check_perm(current_user, Permission.COMMENT_THREAD) else None
if not current_user.canCommentRL():
flash("Please wait before commenting again", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("homepage.home"))
# Check that title is none to load comments into textarea if redirected from new thread page
if form and form.validate_on_submit() and request.form.get("title") is None:
comment = form.comment.data
if len(comment) <= 500 and len(comment) > 3:
reply = ThreadReply()
reply.author = current_user
reply.comment = comment
db.session.add(reply)
if not current_user.can_comment_ratelimit():
flash(gettext("Please wait before commenting again"), "danger")
return redirect(thread.get_view_url())
thread.replies.append(reply)
if not current_user in thread.watchers:
thread.watchers.append(current_user)
if has_blocked_domains(comment, current_user.username, f"reply to {thread.get_view_url(True)}"):
flash(gettext("Linking to blocked sites is not allowed"), "danger")
return render_template("threads/view.html", thread=thread, form=form)
msg = None
if thread.package is None:
msg = "New comment on '{}'".format(thread.title)
else:
msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
reply = ThreadReply()
reply.author = current_user
reply.comment = comment
db.session.add(reply)
thread.replies.append(reply)
if current_user not in thread.watchers:
thread.watchers.append(current_user)
for user in thread.watchers:
if user != current_user:
triggerNotif(user, current_user, msg, url_for("threads.view", id=thread.id))
for mentioned_username in get_user_mentions(render_markdown(comment)):
mentioned = User.query.filter_by(username=mentioned_username).first()
if mentioned is None:
continue
db.session.commit()
if not thread.check_perm(mentioned, Permission.SEE_THREAD):
continue
return redirect(url_for("threads.view", id=id))
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
msg, thread.get_view_url(), thread.package)
else:
flash("Comment needs to be between 3 and 500 characters.")
if mentioned not in thread.watchers:
thread.watchers.append(mentioned)
return render_template("threads/view.html", thread=thread)
msg = "New comment on '{}'".format(thread.title)
add_notification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.get_view_url(), thread.package)
if thread.author == get_system_user():
approvers = User.query.filter(User.rank >= UserRank.APPROVER).all()
add_notification(approvers, current_user, NotificationType.EDITOR_MISC, msg,
thread.get_view_url(), thread.package)
post_discord_webhook.delay(current_user.display_name,
"Replied to bot messages: {}".format(thread.get_view_url(absolute=True)), True)
db.session.commit()
return redirect(thread.get_view_url())
return render_template("threads/view.html", thread=thread, form=form)
class ThreadForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)])
comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
private = BooleanField("Private")
submit = SubmitField("Open Thread")
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)])
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
btn_submit = SubmitField(lazy_gettext("Open Thread"))
@bp.route("/threads/new/", methods=["GET", "POST"])
@login_required
@@ -142,80 +296,112 @@ def new():
if "pid" in request.args:
package = Package.query.get(int(request.args.get("pid")))
if package is None:
flash("Unable to find that package!", "danger")
abort(404)
# Don't allow making orphan threads on approved packages for now
if package is None:
abort(403)
if package is None and not current_user.rank.at_least(UserRank.APPROVER):
abort(404)
def_is_private = request.args.get("private") or False
if package is None:
def_is_private = True
allow_change = package and package.approved
is_review_thread = package and not package.approved
is_private_thread = is_review_thread
# Check that user can make the thread
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
flash("Unable to create thread!", "danger")
if package and not package.check_perm(current_user, Permission.CREATE_THREAD):
flash(gettext("Unable to create thread!"), "danger")
return redirect(url_for("homepage.home"))
# Only allow creating one thread when not approved
elif is_review_thread and package.review_thread is not None:
flash("A review thread already exists!", "danger")
return redirect(url_for("threads.view", id=package.review_thread.id))
# Redirect submit to `view` page, which checks for `title` in the form data and so won't commit the reply
flash(gettext("An approval thread already exists! Consider replying there instead"), "danger")
return redirect(package.review_thread.get_view_url(), code=307)
elif not current_user.canOpenThreadRL():
flash("Please wait before opening another thread", "danger")
elif not current_user.can_open_thread_ratelimit():
flash(gettext("Please wait before opening another thread"), "danger")
if package:
return redirect(package.getDetailsURL())
return redirect(package.get_url("packages.view"))
else:
return redirect(url_for("homepage.home"))
# Set default values
elif request.method == "GET":
form.private.data = def_is_private
form.title.data = request.args.get("title") or ""
# Validate and submit
elif request.method == "POST" and form.validate():
thread = Thread()
thread.author = current_user
thread.title = form.title.data
thread.private = form.private.data if allow_change else def_is_private
thread.package = package
db.session.add(thread)
thread.watchers.append(current_user)
if package is not None and package.author != current_user:
thread.watchers.append(package.author)
reply = ThreadReply()
reply.thread = thread
reply.author = current_user
reply.comment = form.comment.data
db.session.add(reply)
thread.replies.append(reply)
db.session.commit()
if is_review_thread:
package.review_thread = thread
notif_msg = None
if package is not None:
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id))
elif form.validate_on_submit():
if has_blocked_domains(form.comment.data, current_user.username, f"new thread"):
flash(gettext("Linking to blocked sites is not allowed"), "danger")
else:
thread = Thread()
thread.author = current_user
thread.title = form.title.data
thread.private = is_private_thread
thread.package = package
db.session.add(thread)
thread.watchers.append(current_user)
if package and package.author != current_user:
thread.watchers.append(package.author)
reply = ThreadReply()
reply.thread = thread
reply.author = current_user
reply.comment = form.comment.data
db.session.add(reply)
thread.replies.append(reply)
db.session.commit()
if is_review_thread:
package.review_thread = thread
for mentioned_username in get_user_mentions(render_markdown(form.comment.data)):
mentioned = User.query.filter_by(username=mentioned_username).first()
if mentioned is None:
continue
msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title)
add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.get_view_url(), thread.package)
if mentioned not in thread.watchers:
thread.watchers.append(mentioned)
notif_msg = "New thread '{}'".format(thread.title)
if package is not None:
add_notification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.get_view_url(), package)
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
triggerNotif(user, current_user, notif_msg, url_for("threads.view", id=thread.id))
approvers = User.query.filter(User.rank >= UserRank.APPROVER).all()
add_notification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.get_view_url(), package)
db.session.commit()
if is_review_thread:
post_discord_webhook.delay(current_user.display_name,
"Opened approval thread: {}".format(thread.get_view_url(absolute=True)), True)
return redirect(url_for("threads.view", id=thread.id))
db.session.commit()
return redirect(thread.get_view_url())
return render_template("threads/new.html", form=form, package=package)
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
@bp.route("/users/<username>/comments/")
def user_comments(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 40))
# Filter replies the current user can see
query = ThreadReply.query.options(selectinload(ThreadReply.thread)).filter_by(author=user)
only_public = False
if current_user != user and not (current_user.is_authenticated and current_user.rank.at_least(UserRank.APPROVER)):
query = query.filter(ThreadReply.thread.has(private=False))
only_public = True
pagination = query.order_by(db.desc(ThreadReply.created_at)).paginate(page=page, per_page=num)
return render_template("threads/user_comments.html", user=user, pagination=pagination, only_public=only_public)

View File

@@ -1,79 +1,145 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
bp = Blueprint("thumbnails", __name__)
import re
import requests
from flask import abort, send_file, Blueprint, current_app, request
import os
from PIL import Image
ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
bp = Blueprint("thumbnails", __name__)
ALLOWED_RESOLUTIONS = [(100, 67), (270, 180), (350, 233), (1100, 520)]
ALLOWED_MIMETYPES = {
"png": "image/png",
"webp": "image/webp",
"jpg": "image/jpeg",
}
def mkdir(path):
assert path != "" and path is not None
if not os.path.isdir(path):
os.mkdir(path)
try:
if not os.path.isdir(path):
os.mkdir(path)
except FileExistsError:
pass
def resize_and_crop(img_path, modified_path, size):
img = Image.open(img_path)
with Image.open(img_path) as img:
# Get current and desired ratio for the images
img_ratio = img.size[0] / float(img.size[1])
desired_ratio = size[0] / float(size[1])
# Get current and desired ratio for the images
img_ratio = img.size[0] / float(img.size[1])
ratio = size[0] / float(size[1])
# Is more portrait than target, scale and crop
if desired_ratio > img_ratio:
img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
Image.BICUBIC)
box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
img = img.crop(box)
# Is more portrait than target, scale and crop
if ratio > img_ratio:
img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
Image.BICUBIC)
box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
img = img.crop(box)
# Is more landscape than target, scale and crop
elif desired_ratio < img_ratio:
img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
Image.BICUBIC)
box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
img = img.crop(box)
# Is more landscape than target, scale and crop
elif ratio < img_ratio:
img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
Image.BICUBIC)
box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
img = img.crop(box)
# Is exactly the same ratio as target
else:
img = img.resize(size, Image.BICUBIC)
# Is exactly the same ratio as target
else:
img = img.resize(size, Image.BICUBIC)
if modified_path.endswith(".jpg") and img.mode != "RGB":
img = img.convert("RGB")
img.save(modified_path)
img.save(modified_path, lossless=True)
def find_source_file(img):
upload_dir = current_app.config["UPLOAD_DIR"]
source_filepath = os.path.join(upload_dir, img)
if os.path.isfile(source_filepath):
return source_filepath
period = source_filepath.rfind(".")
start = source_filepath[:period]
ext = source_filepath[period + 1:]
if ext not in ALLOWED_MIMETYPES:
abort(404)
for other_ext in ALLOWED_MIMETYPES.keys():
other_path = f"{start}.{other_ext}"
if ext != other_ext and os.path.isfile(other_path):
return other_path
abort(404)
def get_mimetype(cache_filepath: str) -> str:
period = cache_filepath.rfind(".")
ext = cache_filepath[period + 1:]
mimetype = ALLOWED_MIMETYPES.get(ext)
if mimetype is None:
abort(404)
return mimetype
@bp.route("/thumbnails/<int:level>/<img>")
def make_thumbnail(img, level):
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
abort(403)
w, h = ALLOWED_RESOLUTIONS[level - 1]
upload_dir = current_app.config["UPLOAD_DIR"]
thumbnail_dir = current_app.config["THUMBNAIL_DIR"]
mkdir(thumbnail_dir)
output_dir = os.path.join(thumbnail_dir, str(level))
mkdir(output_dir)
cache_filepath = os.path.join(output_dir, img)
source_filepath = os.path.join(upload_dir, img)
cache_filepath = os.path.join(output_dir, img)
if not os.path.isfile(cache_filepath):
source_filepath = find_source_file(img)
resize_and_crop(source_filepath, cache_filepath, (w, h))
resize_and_crop(source_filepath, cache_filepath, (w, h))
return send_file(cache_filepath)
res = send_file(cache_filepath, mimetype=get_mimetype(cache_filepath))
res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res
@bp.route("/thumbnails/youtube/<id_>.jpg")
def youtube(id_: str):
if not re.match(r"^[A-Za-z0-9\-_]+$", id_):
abort(400)
cache_dir = os.path.join(current_app.config["THUMBNAIL_DIR"], "youtube")
os.makedirs(cache_dir, exist_ok=True)
cache_filepath = os.path.join(cache_dir, id_ + ".jpg")
url = f"https://img.youtube.com/vi/{id_}/default.jpg"
response = requests.get(url, stream=True)
if response.status_code != 200:
abort(response.status_code)
with open(cache_filepath, "wb") as file:
file.write(response.content)
res = send_file(cache_filepath)
res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res

View File

@@ -1,102 +1,22 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask import *
from flask_user import *
import flask_menu as menu
from app.models import *
from app.querybuilder import QueryBuilder
from app.utils import get_int_or_abort
from flask import Blueprint
bp = Blueprint("todo", __name__)
@bp.route("/todo/", methods=["GET", "POST"])
@login_required
def view():
canApproveNew = Permission.APPROVE_NEW.check(current_user)
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
packages = None
if canApproveNew:
packages = Package.query.filter_by(approved=False, soft_deleted=False).order_by(db.desc(Package.created_at)).all()
releases = None
if canApproveRel:
releases = PackageRelease.query.filter_by(approved=False).all()
screenshots = None
if canApproveScn:
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
if not canApproveNew and not canApproveRel and not canApproveScn:
abort(403)
if request.method == "POST":
if request.form["action"] == "screenshots_approve_all":
if not canApproveScn:
abort(403)
PackageScreenshot.query.update({ "approved": True })
db.session.commit()
return redirect(url_for("todo.view"))
else:
abort(400)
topic_query = ForumTopic.query \
.filter_by(discarded=False)
total_topics = topic_query.count()
topics_to_add = topic_query \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.count()
return render_template("todo/list.html", title="Reports and Work Queue",
packages=packages, releases=releases, screenshots=screenshots,
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
topics_to_add=topics_to_add, total_topics=total_topics)
@bp.route("/todo/topics/")
@login_required
def topics():
qb = QueryBuilder(request.args)
qb.setSortIfNone("date")
query = qb.buildTopicQuery()
tmp_q = ForumTopic.query
if not qb.show_discarded:
tmp_q = tmp_q.filter_by(discarded=False)
total = tmp_q.count()
topic_count = query.count()
page = get_int_or_abort(request.args.get("page"), 1)
num = get_int_or_abort(request.args.get("n"), 100)
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
num = 100
query = query.paginate(page, num, True)
next_url = url_for("todo.topics", page=query.next_num, query=qb.search, \
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
if query.has_next else None
prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search, \
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
if query.has_prev else None
return render_template("todo/topics.html", topics=query.items, total=total, \
topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
n=num, sort_by=qb.order_by)
from . import editor, user

View File

@@ -0,0 +1,217 @@
# ContentDB
# Copyright (C) 2018-23 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 flask import redirect, url_for, abort, render_template, request
from flask_login import current_user, login_required
from sqlalchemy import or_, and_
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, LuantiRelease, Report
from app.querybuilder import QueryBuilder
from app.utils import get_int_or_abort, is_yes, rank_required
from . import bp
@bp.route("/todo/", methods=["GET", "POST"])
@login_required
def view_editor():
can_approve_new = Permission.APPROVE_NEW.check(current_user)
can_approve_rel = Permission.APPROVE_RELEASE.check(current_user)
can_approve_scn = Permission.APPROVE_SCREENSHOT.check(current_user)
packages = None
wip_packages = None
if can_approve_new:
packages = Package.query.filter_by(state=PackageState.READY_FOR_REVIEW) \
.order_by(db.desc(Package.created_at)).all()
wip_packages = Package.query \
.filter(or_(Package.state == PackageState.WIP, Package.state == PackageState.CHANGES_NEEDED)) \
.order_by(db.desc(Package.created_at)).all()
releases = None
if can_approve_rel:
releases = PackageRelease.query.filter_by(approved=False, task_id=None).all()
screenshots = None
if can_approve_scn:
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
if not can_approve_new and not can_approve_rel and not can_approve_scn:
abort(403)
if request.method == "POST":
if request.form["action"] == "screenshots_approve_all":
if not can_approve_scn:
abort(403)
PackageScreenshot.query.update({"approved": True})
db.session.commit()
return redirect(url_for("todo.view_editor"))
else:
abort(400)
license_needed = Package.query \
.filter(Package.state.in_([PackageState.READY_FOR_REVIEW, PackageState.APPROVED])) \
.filter(or_(Package.license.has(License.name.like("Other %")),
Package.media_license.has(License.name.like("Other %")))) \
.all()
total_packages = Package.query.filter_by(state=PackageState.APPROVED).count()
total_to_tag = Package.query.filter_by(state=PackageState.APPROVED, tags=None).count()
unfulfilled_meta_packages = MetaPackage.query \
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
.filter(MetaPackage.dependencies.any(Dependency.depender.has(state=PackageState.APPROVED), optional=False)) \
.order_by(db.asc(MetaPackage.name)).count()
audit_log = AuditLogEntry.query \
.filter(AuditLogEntry.package.has()) \
.order_by(db.desc(AuditLogEntry.created_at)) \
.limit(20).all()
reports = Report.query.filter_by(is_resolved=False).order_by(db.asc(Report.created_at)).all() if current_user.rank.at_least(UserRank.EDITOR) else None
return render_template("todo/editor.html", current_tab="editor",
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
can_approve_new=can_approve_new, can_approve_rel=can_approve_rel, can_approve_scn=can_approve_scn,
license_needed=license_needed, total_packages=total_packages, total_to_tag=total_to_tag,
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log, reports=reports)
@bp.route("/todo/tags/")
@login_required
def tags():
qb = QueryBuilder(request.args, cookies=True)
qb.set_sort_if_none("score", "desc")
query = qb.build_package_query()
only_no_tags = is_yes(request.args.get("no_tags"))
if only_no_tags:
query = query.filter(Package.tags == None)
tags = Tag.query.order_by(db.asc(Tag.title)).all()
return render_template("todo/tags.html", current_tab="tags", packages=query.all(),
tags=tags, only_no_tags=only_no_tags)
@bp.route("/todo/modnames/")
@login_required
def modnames():
mnames = MetaPackage.query \
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
.filter(MetaPackage.dependencies.any(Dependency.depender.has(state=PackageState.APPROVED), optional=False)) \
.order_by(db.asc(MetaPackage.name)).all()
return render_template("todo/modnames.html", modnames=mnames)
@bp.route("/todo/outdated/")
@login_required
def outdated():
is_mtm_only = is_yes(request.args.get("mtm"))
query = db.session.query(Package).select_from(PackageUpdateConfig) \
.filter(PackageUpdateConfig.outdated_at.isnot(None)) \
.join(PackageUpdateConfig.package) \
.filter(Package.state == PackageState.APPROVED)
if is_mtm_only:
query = query.filter(Package.repo.ilike("%github.com/minetest-mods/%"))
sort_by = request.args.get("sort")
if sort_by == "date":
query = query.order_by(db.desc(PackageUpdateConfig.outdated_at))
else:
sort_by = "score"
query = query.order_by(db.desc(Package.score))
return render_template("todo/outdated.html", current_tab="outdated",
outdated_packages=query.all(), sort_by=sort_by, is_mtm_only=is_mtm_only)
@bp.route("/todo/screenshots/")
@login_required
def screenshots():
is_mtm_only = is_yes(request.args.get("mtm"))
query = db.session.query(Package) \
.filter(~Package.screenshots.any()) \
.filter(Package.state == PackageState.APPROVED)
if is_mtm_only:
query = query.filter(Package.repo.ilike("%github.com/minetest-mods/%"))
sort_by = request.args.get("sort")
if sort_by == "date":
query = query.order_by(db.desc(Package.approved_at))
else:
sort_by = "score"
query = query.order_by(db.desc(Package.score))
return render_template("todo/screenshots.html", current_tab="screenshots",
packages=query.all(), sort_by=sort_by, is_mtm_only=is_mtm_only)
@bp.route("/todo/mtver_support/")
@login_required
def mtver_support():
is_mtm_only = is_yes(request.args.get("mtm"))
current_stable = LuantiRelease.query.filter(~LuantiRelease.name.like("%-dev")).order_by(db.desc(LuantiRelease.id)).first()
query = db.session.query(Package) \
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \
.filter(Package.state == PackageState.APPROVED)
if is_mtm_only:
query = query.filter(Package.repo.ilike("%github.com/minetest-mods/%"))
sort_by = request.args.get("sort")
if sort_by == "date":
query = query.order_by(db.desc(Package.approved_at))
else:
sort_by = "score"
query = query.order_by(db.desc(Package.score))
return render_template("todo/mtver_support.html", current_tab="screenshots",
packages=query.all(), sort_by=sort_by, is_mtm_only=is_mtm_only, current_stable=current_stable)
@bp.route("/todo/topics/mismatch/")
@rank_required(UserRank.EDITOR)
def topics_mismatch():
missing_topics = Package.query.filter(Package.forums.is_not(None)) .filter(~ForumTopic.query.filter(ForumTopic.topic_id == Package.forums).exists()).all()
packages_bad_author = (
db.session.query(Package, ForumTopic)
.select_from(Package)
.join(ForumTopic, Package.forums == ForumTopic.topic_id)
.filter(Package.author_id != ForumTopic.author_id)
.all())
packages_bad_title = (
db.session.query(Package, ForumTopic)
.select_from(Package)
.join(ForumTopic, Package.forums == ForumTopic.topic_id)
.filter(and_(ForumTopic.name != Package.name, ~ForumTopic.title.ilike("%" + Package.title + "%"), ~ForumTopic.title.ilike("%" + Package.name + "%")))
.all())
return render_template("todo/topics_mismatch.html",
missing_topics=missing_topics,
packages_bad_author=packages_bad_author,
packages_bad_title=packages_bad_title)

194
app/blueprints/todo/user.py Normal file
View File

@@ -0,0 +1,194 @@
# ContentDB
# Copyright (C) 2018-23 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 celery import uuid
from flask import redirect, url_for, abort, render_template, flash
from flask_babel import gettext
from flask_login import current_user, login_required
from sqlalchemy import or_, and_
from app.models import User, Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType
from app.tasks.importtasks import make_vcs_release
from app.utils import add_notification, add_audit_log
from . import bp
@bp.route("/user/tags/")
def tags_user():
return redirect(url_for('todo.tags', author=current_user.username))
@bp.route("/user/todo/")
@bp.route("/users/<username>/todo/")
@login_required
def view_user(username=None):
if username is None:
return redirect(url_for("todo.view_user", username=current_user.username))
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if current_user != user and not current_user.rank.at_least(UserRank.APPROVER):
abort(403)
unapproved_packages = user.packages \
.filter(or_(Package.state == PackageState.WIP,
Package.state == PackageState.CHANGES_NEEDED)) \
.order_by(db.asc(Package.created_at)).all()
outdated_packages = user.maintained_packages \
.filter(Package.state != PackageState.DELETED,
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
.order_by(db.asc(Package.title)).all()
missing_game_support = user.maintained_packages.filter(
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False) \
.order_by(db.asc(Package.title)).all()
packages_with_no_screenshots = user.maintained_packages.filter(
~Package.screenshots.any(), Package.state == PackageState.APPROVED).all()
packages_with_small_screenshots = user.maintained_packages \
.filter(Package.state != PackageState.DELETED,
Package.screenshots.any(and_(PackageScreenshot.width < PackageScreenshot.SOFT_MIN_SIZE[0],
PackageScreenshot.height < PackageScreenshot.SOFT_MIN_SIZE[1]))) \
.all()
topics_to_add = ForumTopic.query \
.filter_by(author_id=user.id) \
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
needs_tags = user.maintained_packages \
.filter(Package.state != PackageState.DELETED, ~Package.tags.any()) \
.order_by(db.asc(Package.title)).all()
return render_template("todo/user.html", current_tab="user", user=user,
unapproved_packages=unapproved_packages, outdated_packages=outdated_packages,
missing_game_support=missing_game_support, needs_tags=needs_tags, topics_to_add=topics_to_add,
packages_with_no_screenshots=packages_with_no_screenshots,
packages_with_small_screenshots=packages_with_small_screenshots,
screenshot_min_size=PackageScreenshot.HARD_MIN_SIZE, screenshot_rec_size=PackageScreenshot.SOFT_MIN_SIZE)
@bp.route("/users/<username>/update-configs/apply-all/", methods=["POST"])
@login_required
def apply_all_updates(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403)
outdated_packages = user.maintained_packages \
.filter(Package.state != PackageState.DELETED,
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
.order_by(db.asc(Package.title)).all()
for package in outdated_packages:
if not package.check_perm(current_user, Permission.MAKE_RELEASE):
continue
if package.releases.filter(or_(PackageRelease.task_id.isnot(None),
PackageRelease.commit_hash == package.update_config.last_commit)).count() > 0:
continue
title = package.update_config.title
ref = package.update_config.get_ref()
rel = PackageRelease()
rel.package = package
rel.name = title
rel.title = title
rel.url = ""
rel.task_id = uuid()
db.session.add(rel)
db.session.commit()
make_vcs_release.apply_async((rel.id, ref),
task_id=rel.task_id)
msg = "Created release {} (Applied all Git Update Detection)".format(rel.title)
add_notification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg,
package.get_url("packages.create_edit"), package)
add_audit_log(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package)
db.session.commit()
return redirect(url_for("todo.view_user", username=username))
@bp.route("/user/game_support/")
@bp.route("/users/<username>/game_support/")
@login_required
def all_game_support(username=None):
if username is None:
return redirect(url_for("todo.all_game_support", username=current_user.username))
user: User = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403)
packages = user.maintained_packages.filter(
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP])) \
.order_by(db.asc(Package.title)).all()
bulk_support_names = db.session.query(Package.title) \
.select_from(Package).filter(
Package.maintainers.contains(user),
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False) \
.order_by(db.asc(Package.title)).all()
bulk_support_names = ", ".join([x[0] for x in bulk_support_names])
return render_template("todo/game_support.html", user=user, packages=packages, bulk_support_names=bulk_support_names)
@bp.route("/users/<username>/confirm_supports_all_games/", methods=["POST"])
@login_required
def confirm_supports_all_games(username=None):
user: User = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403)
packages = user.maintained_packages.filter(
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False) \
.all()
for package in packages:
package.supports_all_games = True
db.session.merge(package)
add_audit_log(AuditSeverity.NORMAL, current_user, "Enabled 'Supports all games' (bulk)",
package.get_url("packages.game_support"), package)
db.session.commit()
flash(gettext("Done"), "success")
return redirect(url_for("todo.all_game_support", username=current_user.username))

View File

@@ -0,0 +1,48 @@
# ContentDB
# Copyright (C) 2024 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 flask import Blueprint, render_template, request
from sqlalchemy import or_
from app.models import Package, PackageState, db, PackageTranslation
bp = Blueprint("translate", __name__)
@bp.route("/translate/")
def translate():
query = Package.query.filter(
Package.state == PackageState.APPROVED,
or_(
Package.translation_url.is_not(None),
Package.translations.any(PackageTranslation.language_id != "en")
))
has_langs = request.args.getlist("has_lang")
for lang in has_langs:
query = query.filter(Package.translations.any(PackageTranslation.language_id == lang))
not_langs = request.args.getlist("not_lang")
for lang in not_langs:
query = query.filter(~Package.translations.any(PackageTranslation.language_id == lang))
supports_translation = (query
.order_by(Package.translation_url.is_(None), db.desc(Package.score))
.all())
return render_template("translate/index.html",
supports_translation=supports_translation, has_langs=has_langs, not_langs=not_langs)

View File

@@ -2,4 +2,4 @@ from flask import Blueprint
bp = Blueprint("users", __name__)
from . import profile, claim
from . import profile, claim, account, settings

View File

@@ -0,0 +1,391 @@
# ContentDB
# Copyright (C) 2020 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 datetime
from flask import redirect, abort, render_template, flash, request, url_for, Response
from flask_babel import gettext, get_locale, lazy_gettext
from flask_login import current_user, login_required, logout_user, login_user
from flask_wtf import FlaskForm
from sqlalchemy import or_
from wtforms import StringField, SubmitField, BooleanField, PasswordField, validators
from wtforms.validators import InputRequired, Length, Regexp, DataRequired, Optional, Email, EqualTo
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email
from app.utils import random_string, make_flask_login_password, is_safe_url, check_password_hash, add_audit_log, \
nonempty_or_none, post_login
from . import bp
from app.models import User, AuditSeverity, db, EmailSubscription, UserEmailVerification
from app.logic.users import create_user
class LoginForm(FlaskForm):
username = StringField(lazy_gettext("Username or email"), [InputRequired()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)])
remember_me = BooleanField(lazy_gettext("Remember me"), default=True)
submit = SubmitField(lazy_gettext("Sign in"))
def handle_login(form):
def show_safe_err(err):
if "@" in username:
flash(gettext("Incorrect email or password"), "danger")
else:
flash(err, "danger")
username = form.username.data.strip()
user = User.query.filter(or_(User.username == username, User.email == username)).first()
if user is None:
return show_safe_err(gettext(u"User %(username)s does not exist", username=username))
if not check_password_hash(user.password, form.password.data):
return show_safe_err(gettext(u"Incorrect password. Did you set one?"))
if not user.is_active:
flash(gettext("You need to confirm the registration email"), "danger")
return
add_audit_log(AuditSeverity.USER, user, "Logged in using password",
url_for("users.profile", username=user.username))
db.session.commit()
if not login_user(user, remember=form.remember_me.data):
flash(gettext("Login failed"), "danger")
return
return post_login(user, request.args.get("next"))
@bp.route("/user/login/", methods=["GET", "POST"])
def login():
next = request.args.get("next")
if next and not is_safe_url(next):
abort(400)
if current_user.is_authenticated:
return redirect(next or url_for("homepage.home"))
form = LoginForm(request.form)
if form.validate_on_submit():
ret = handle_login(form)
if ret:
return ret
if request.method == "GET":
form.remember_me.data = True
return render_template("users/login.html", form=form, next=next)
@bp.route("/user/logout/", methods=["GET", "POST"])
def logout():
logout_user()
return redirect(url_for("homepage.home"))
class RegisterForm(FlaskForm):
display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[nonempty_or_none])
username = StringField(lazy_gettext("Username"), [InputRequired(),
Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext(
"Only alphabetic letters (A-Za-z), numbers (0-9), underscores (_), minuses (-), and periods (.) allowed"))])
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
first_name = StringField("First name", [])
submit = SubmitField(lazy_gettext("Register"))
def handle_register(form):
if form.question.data.strip().lower() != "19":
flash(gettext("Incorrect captcha answer"), "danger")
return
user = create_user(form.username.data, form.display_name.data, form.email.data)
if isinstance(user, Response):
return user
elif user is None:
return
elif form.first_name.data != "":
abort(500)
user.password = make_flask_login_password(form.password.data)
add_audit_log(AuditSeverity.USER, user, "Registered with email, display name=" + user.display_name,
url_for("users.profile", username=user.username))
token = random_string(32)
ver = UserEmailVerification()
ver.user = user
ver.token = token
ver.email = form.email.data
db.session.add(ver)
db.session.commit()
send_verify_email.delay(form.email.data, token, get_locale().language)
return redirect(url_for("users.email_sent"))
@bp.route("/user/register/", methods=["GET", "POST"])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
ret = handle_register(form)
if ret:
return ret
return render_template("users/register.html", form=form)
class ForgotPasswordForm(FlaskForm):
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
submit = SubmitField(lazy_gettext("Reset Password"))
@bp.route("/user/forgot-password/", methods=["GET", "POST"])
def forgot_password():
form = ForgotPasswordForm(request.form)
if form.validate_on_submit():
email = form.email.data
user = User.query.filter_by(email=email).first()
if user:
token = random_string(32)
add_audit_log(AuditSeverity.USER, user, "(Anonymous) requested a password reset",
url_for("users.profile", username=user.username), None)
ver = UserEmailVerification()
ver.user = user
ver.token = token
ver.email = email
ver.is_password_reset = True
db.session.add(ver)
db.session.commit()
send_verify_email.delay(form.email.data, token, get_locale().language)
else:
html = render_template("emails/unable_to_find_account.html")
send_anon_email.delay(email, get_locale().language, gettext("Unable to find account"),
html, html)
return redirect(url_for("users.email_sent"))
return render_template("users/forgot_password.html", form=form)
class SetPasswordForm(FlaskForm):
email = StringField(lazy_gettext("Email"), [Optional(), Email()])
password = PasswordField(lazy_gettext("New password"), [InputRequired(), Length(12, 100)])
password2 = PasswordField(lazy_gettext("Verify password"), [InputRequired(), Length(12, 100),
EqualTo('password', message=lazy_gettext('Passwords must match'))])
submit = SubmitField(lazy_gettext("Save"))
class ChangePasswordForm(FlaskForm):
old_password = PasswordField(lazy_gettext("Old password"), [InputRequired(), Length(6, 100)])
password = PasswordField(lazy_gettext("New password"), [InputRequired(), Length(12, 100)])
password2 = PasswordField(lazy_gettext("Verify password"), [InputRequired(), Length(12, 100),
validators.EqualTo('password', message=lazy_gettext('Passwords must match'))])
submit = SubmitField(lazy_gettext("Save"))
def handle_set_password(form):
one = form.password.data
two = form.password2.data
if one != two:
flash(gettext("Passwords do not match"), "danger")
return
add_audit_log(AuditSeverity.USER, current_user, "Changed their password", url_for("users.profile", username=current_user.username))
current_user.password = make_flask_login_password(form.password.data)
if hasattr(form, "email"):
new_email = nonempty_or_none(form.email.data)
if new_email and new_email != current_user.email:
if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
flash(gettext(u"That email address has been unsubscribed/blacklisted, and cannot be used"), "danger")
return
user_by_email = User.query.filter_by(email=form.email.data).first()
if user_by_email:
send_anon_email.delay(form.email.data, get_locale().language, gettext("Email already in use"),
gettext(u"We were unable to create the account as the email is already in use by %(display_name)s. Try a different email address.",
display_name=user_by_email.display_name))
else:
token = random_string(32)
ver = UserEmailVerification()
ver.user = current_user
ver.token = token
ver.email = new_email
db.session.add(ver)
db.session.commit()
send_verify_email.delay(form.email.data, token, get_locale().language)
flash(gettext("Your password has been changed successfully."), "success")
return redirect(url_for("users.email_sent"))
db.session.commit()
flash(gettext("Your password has been changed successfully."), "success")
return redirect(url_for("homepage.home"))
@bp.route("/user/change-password/", methods=["GET", "POST"])
@login_required
def change_password():
form = ChangePasswordForm(request.form)
if form.validate_on_submit():
if check_password_hash(current_user.password, form.old_password.data):
ret = handle_set_password(form)
if ret:
return ret
else:
flash(gettext("Old password is incorrect"), "danger")
return render_template("users/change_set_password.html", form=form)
@bp.route("/user/set-password/", methods=["GET", "POST"])
@login_required
def set_password():
if current_user.password:
return redirect(url_for("users.change_password"))
form = SetPasswordForm(request.form)
if current_user.email is None:
form.email.validators = [InputRequired(), Email()]
if form.validate_on_submit():
ret = handle_set_password(form)
if ret:
return ret
return render_template("users/change_set_password.html", form=form)
@bp.route("/user/verify/")
def verify_email():
token = request.args.get("token")
ver: UserEmailVerification = UserEmailVerification.query.filter_by(token=token).first()
if ver is None:
flash(gettext("Unknown verification token!"), "danger")
return redirect(url_for("homepage.home"))
if ver.is_expired:
flash(gettext("Token has expired"), "danger")
db.session.delete(ver)
db.session.commit()
return redirect(url_for("homepage.home"))
user = ver.user
add_audit_log(AuditSeverity.USER, user, "Confirmed their email",
url_for("users.profile", username=user.username))
was_activating = not user.is_active
if ver.email and user.email != ver.email:
if User.query.filter_by(email=ver.email).count() > 0:
flash(gettext("Another user is already using that email"), "danger")
return redirect(url_for("homepage.home"))
flash(gettext("Confirmed email change"), "success")
if user.email:
send_user_email.delay(user.email,
user.locale or "en",
gettext("Email address changed"),
gettext("Your email address has changed. If you didn't request this, please contact an administrator."))
user.is_active = True
user.email = ver.email
db.session.delete(ver)
db.session.commit()
if ver.is_password_reset:
login_user(user)
user.password = None
db.session.commit()
return redirect(url_for("users.set_password"))
if current_user.is_authenticated:
return redirect(url_for("users.profile", username=current_user.username))
elif was_activating:
flash(gettext("You may now log in"), "success")
return redirect(url_for("users.login"))
else:
return redirect(url_for("homepage.home"))
class UnsubscribeForm(FlaskForm):
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
submit = SubmitField(lazy_gettext("Send"))
def unsubscribe_verify():
form = UnsubscribeForm(request.form)
if form.validate_on_submit():
email = form.email.data
sub = EmailSubscription.query.filter_by(email=email).first()
if not sub:
sub = EmailSubscription(email)
db.session.add(sub)
sub.token = random_string(32)
db.session.commit()
send_unsubscribe_verify.delay(form.email.data, get_locale().language)
return redirect(url_for("users.email_sent"))
return render_template("users/unsubscribe.html", form=form)
def unsubscribe_manage(sub: EmailSubscription):
user = User.query.filter_by(email=sub.email).first()
if request.method == "POST":
if user:
user.email = None
sub.blacklisted = True
db.session.commit()
flash(gettext("That email is now blacklisted. Please contact an admin if you wish to undo this."), "success")
return redirect(url_for("homepage.home"))
return render_template("users/unsubscribe.html", user=user)
@bp.route("/unsubscribe/", methods=["GET", "POST"])
def unsubscribe():
token = request.args.get("token")
if token:
sub = EmailSubscription.query.filter_by(token=token).first()
if sub:
return unsubscribe_manage(sub)
return unsubscribe_verify()
@bp.route("/email_sent/")
def email_sent():
return render_template("users/email_sent.html")

View File

@@ -1,98 +1,118 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
from flask_babel import gettext
from flask_login import current_user
from . import bp
from flask import redirect, render_template, session, request, flash, url_for
from flask_user import current_user
from app.models import db, User, UserRank
from app.utils import randomString, loginUser, rank_required
from app.tasks.forumtasks import checkForumAccount
from app.tasks.phpbbparser import getProfile
from app.utils import random_string, login_user_set_active
from app.tasks.forumtasks import check_forum_account
from app.utils.phpbbparser import get_profile
@bp.route("/user/claim/", methods=["GET", "POST"])
def claim():
return render_template("users/claim.html")
@bp.route("/user/claim-forums/", methods=["GET", "POST"])
def claim_forums():
if current_user.is_authenticated:
return redirect(url_for("homepage.home"))
username = request.args.get("username")
if username is None:
username = ""
else:
method = request.args.get("method")
user = User.query.filter_by(forums_username=username).first()
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
flash("User has already been claimed", "danger")
return redirect(url_for("users.claim"))
elif user is None and method == "github":
flash("Unable to get Github username for user", "danger")
return redirect(url_for("users.claim"))
elif user is None:
flash("Unable to find that user", "danger")
return redirect(url_for("users.claim"))
if user and user.rank.at_least(UserRank.NEW_MEMBER):
flash(gettext("User has already been claimed"), "danger")
return redirect(url_for("users.claim_forums"))
elif method == "github":
if user is None or user.github_username is None:
flash(gettext("Unable to get GitHub username for user. Make sure the forum account exists."), "danger")
return redirect(url_for("users.claim_forums", username=username))
else:
return redirect(url_for("vcs.github_start"))
if user is not None and method == "github":
return redirect(url_for("github.start"))
token = None
if "forum_token" in session:
token = session["forum_token"]
else:
token = randomString(32)
token = random_string(12)
session["forum_token"] = token
if request.method == "POST":
ctype = request.form.get("claim_type")
ctype = request.form.get("claim_type")
username = request.form.get("username")
if username is None or len(username.strip()) < 2:
flash("Invalid username", "danger")
elif ctype == "github":
task = checkForumAccount.delay(username)
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim", username=username, method="github")))
if User.query.filter(User.username == username, User.forums_username.is_(None)).first():
flash(gettext("A ContentDB user with that name already exists. Please contact an admin to link to your forum account"), "danger")
return redirect(url_for("users.claim_forums"))
if ctype == "github":
task = check_forum_account.delay(username)
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim_forums", username=username, method="github")))
elif ctype == "forum":
user = User.query.filter_by(forums_username=username).first()
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
flash("That user has already been claimed!", "danger")
return redirect(url_for("users.claim"))
if user is not None and user.rank.at_least(UserRank.NEW_MEMBER):
flash(gettext("That user has already been claimed!"), "danger")
return redirect(url_for("users.claim_forums"))
# Get signature
sig = None
try:
profile = getProfile("https://forum.minetest.net", username)
sig = profile.signature
except IOError:
flash("Unable to get forum signature - does the user exist?", "danger")
return redirect(url_for("users.claim", username=username))
profile = get_profile("https://forum.luanti.org", username)
sig = profile.signature if profile else None
except IOError as e:
if hasattr(e, 'message'):
message = e.message
else:
message = str(e)
flash(gettext(u"Error whilst attempting to access forums: %(message)s", message=message), "danger")
return redirect(url_for("users.claim_forums", username=username))
if profile is None:
flash(gettext("Unable to get forum signature - does the user exist?"), "danger")
return redirect(url_for("users.claim_forums", username=username))
# Look for key
if token in sig:
if sig and token in sig:
# Try getting again to fix crash
user = User.query.filter_by(forums_username=username).first()
if user is None:
user = User(username)
user.forums_username = username
db.session.add(user)
db.session.commit()
if loginUser(user):
return redirect(url_for("users.set_password"))
else:
flash("Unable to login as user", "danger")
return redirect(url_for("users.claim", username=username))
ret = login_user_set_active(user, remember=True)
if ret is None:
flash(gettext("Unable to login as user"), "danger")
return redirect(url_for("users.claim_forums", username=username))
return ret
else:
flash("Could not find the key in your signature!", "danger")
return redirect(url_for("users.claim", username=username))
flash(gettext("Could not find the key in your signature!"), "danger")
return redirect(url_for("users.claim_forums", username=username))
else:
flash("Unknown claim type", "danger")
flash(gettext("Unknown claim type"), "danger")
return render_template("users/claim.html", username=username, key=token)
return render_template("users/claim_forums.html", username=username, key="cdb_" + token)

View File

@@ -1,238 +1,287 @@
# Content DB
# Copyright (C) 2018 rubenwardy
# ContentDB
# Copyright (C) 2018-21 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# 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 General Public License for more details.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU 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/>.
import math
from typing import Optional, Tuple, List
from flask import redirect, url_for, abort, render_template, request
from flask_babel import gettext
from flask_login import current_user, login_required
from sqlalchemy import func, text
from app.models import User, db, Package, PackageReview, PackageState, PackageType, UserRank, Collection
from app.utils import get_daterange_options
from app.tasks.forumtasks import check_forum_account
from flask import *
from flask_user import *
from flask_login import login_user, logout_user
from app.markdown import render_markdown
from . import bp
from app.models import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import randomString, loginUser, rank_required
from app.tasks.forumtasks import checkForumAccount
from app.tasks.emails import sendVerifyEmail, sendEmailRaw
from app.tasks.phpbbparser import getProfile
# Define the User profile form
class UserProfileForm(FlaskForm):
display_name = StringField("Display name", [Optional(), Length(2, 20)])
email = StringField("Email", [Optional(), Email()], filters = [lambda x: x or None])
website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None])
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
submit = SubmitField("Save")
@bp.route("/users/", methods=["GET"])
def list_all():
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
users = db.session.query(User, func.count(Package.id)) \
.select_from(User).outerjoin(Package) \
.order_by(db.desc(User.rank), db.asc(User.display_name)) \
.group_by(User.id).all()
return render_template("users/list.html", users=users)
@bp.route("/users/<username>/", methods=["GET", "POST"])
@bp.route("/user/forum/<username>/")
def by_forums_username(username):
user = User.query.filter_by(forums_username=username).first()
if user:
return redirect(url_for("users.profile", username=user.username))
return render_template("users/forums_no_such_user.html", username=username)
class Medal:
description: str
color: Optional[str]
icon: str
title: Optional[str]
progress: Optional[Tuple[int, int]]
def __init__(self, description: str, **kwargs):
self.description = description
self.color = kwargs.get("color", "white")
self.icon = kwargs.get("icon", None)
self.title = kwargs.get("title", None)
self.progress = kwargs.get("progress", None)
@classmethod
def make_unlocked(cls, color: str, icon: str, title: str, description: str):
return Medal(description=description, color=color, icon=icon, title=title)
@classmethod
def make_locked(cls, description: str, progress: Tuple[int, int]):
if progress[0] is None or progress[1] is None:
raise Exception("Invalid progress")
return Medal(description=description, progress=progress)
def place_to_color(place: int) -> str:
if place == 1:
return "gold"
elif place == 2:
return "#888"
elif place == 3:
return "#cd7f32"
else:
return "white"
def get_user_medals(user: User) -> Tuple[List[Medal], List[Medal]]:
unlocked = []
locked = []
#
# REVIEWS
#
users_by_reviews = db.session.query(User.username, func.sum(PackageReview.score).label("karma")) \
.select_from(User).join(PackageReview) \
.group_by(User.username).order_by(text("karma DESC")).all()
try:
review_boundary = users_by_reviews[math.floor(len(users_by_reviews) * 0.25)][1] + 1
except IndexError:
review_boundary = None
usernames_by_reviews = [username for username, _ in users_by_reviews]
review_idx = None
review_percent = None
review_karma = 0
try:
review_idx = usernames_by_reviews.index(user.username)
review_percent = round(100 * review_idx / len(users_by_reviews), 1)
review_karma = max(users_by_reviews[review_idx][1], 0)
except ValueError:
pass
if review_percent is not None and review_percent < 25:
if review_idx == 0:
title = gettext(u"Top reviewer")
description = gettext(
u"%(display_name)s has written the most helpful reviews on ContentDB.",
display_name=user.display_name)
elif review_idx <= 2:
if review_idx == 1:
title = gettext(u"2nd most helpful reviewer")
else:
title = gettext(u"3rd most helpful reviewer")
description = gettext(
u"This puts %(display_name)s in the top %(perc)s%%",
display_name=user.display_name, perc=review_percent)
else:
title = gettext(u"Top %(perc)s%% reviewer", perc=review_percent)
description = gettext(u"Only %(place)d users have written more helpful reviews.", place=review_idx)
unlocked.append(Medal.make_unlocked(
place_to_color(review_idx + 1), "fa-star-half-alt", title, description))
elif review_boundary is not None:
description = gettext(u"Consider writing more helpful reviews to get a medal.")
if review_idx:
description += " " + gettext(u"You are in place %(place)s.", place=review_idx + 1)
locked.append(Medal.make_locked(
description, (review_karma, review_boundary)))
#
# TOP PACKAGES
#
all_package_ranks = db.session.query(
Package.type,
Package.author_id,
func.rank().over(
order_by=db.desc(Package.score),
partition_by=Package.type) \
.label("rank")).order_by(db.asc(text("rank"))) \
.filter_by(state=PackageState.APPROVED).subquery()
user_package_ranks = db.session.query(all_package_ranks) \
.filter_by(author_id=user.id) \
.filter(text("rank <= 30")) \
.all()
user_package_ranks = next(
(x for x in user_package_ranks if x[0] == PackageType.MOD or x[2] <= 10),
None)
if user_package_ranks:
top_rank = user_package_ranks[2]
top_type = PackageType.coerce(user_package_ranks[0])
title = top_type.get_top_ordinal(top_rank)
if top_type == PackageType.MOD:
icon = "fa-box"
elif top_type == PackageType.GAME:
icon = "fa-gamepad"
else:
icon = "fa-paint-brush"
description = top_type.get_top_ordinal_description(user.display_name, top_rank)
unlocked.append(
Medal.make_unlocked(place_to_color(top_rank), icon, title, description))
#
# DOWNLOADS
#
total_downloads = db.session.query(func.sum(Package.downloads)) \
.select_from(User) \
.join(User.packages) \
.filter(User.id == user.id,
Package.state == PackageState.APPROVED).scalar()
if total_downloads is None:
pass
elif total_downloads < 50000:
description = gettext(u"Your packages have %(downloads)d downloads in total.", downloads=total_downloads)
description += " " + gettext(u"First medal is at 50k.")
locked.append(Medal.make_locked(description, (total_downloads, 50000)))
else:
if total_downloads >= 300000:
place = 1
title = gettext(u">300k downloads")
elif total_downloads >= 100000:
place = 2
title = gettext(u">100k downloads")
elif total_downloads >= 75000:
place = 3
title = gettext(u">75k downloads")
else:
place = 10
title = gettext(u">50k downloads")
description = gettext(u"Has received %(downloads)d downloads across all packages.",
display_name=user.display_name, downloads=total_downloads)
unlocked.append(Medal.make_unlocked(place_to_color(place), "fa-users", title, description))
return unlocked, locked
@bp.route("/users/<username>/")
def profile(username):
user = User.query.filter_by(username=username).first()
if not user:
abort(404)
form = None
if user.checkPerm(current_user, Permission.CHANGE_DNAME) or \
user.checkPerm(current_user, Permission.CHANGE_EMAIL) or \
user.checkPerm(current_user, Permission.CHANGE_RANK):
# Initialize form
form = UserProfileForm(formdata=request.form, obj=user)
if not current_user.is_authenticated or (user != current_user and not current_user.can_access_todo_list()):
packages = user.packages.filter_by(state=PackageState.APPROVED)
maintained_packages = user.maintained_packages.filter_by(state=PackageState.APPROVED)
else:
packages = user.packages.filter(Package.state != PackageState.DELETED)
maintained_packages = user.maintained_packages.filter(Package.state != PackageState.DELETED)
# Process valid POST
if request.method=="POST" and form.validate():
# Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_DNAME):
user.display_name = form["display_name"].data
packages = packages.order_by(db.asc(Package.title)).all()
maintained_packages = maintained_packages \
.filter(Package.author != user) \
.order_by(db.asc(Package.title)).all()
if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS):
user.website_url = form["website_url"].data
user.donate_url = form["donate_url"].data
if user.checkPerm(current_user, Permission.CHANGE_RANK):
newRank = form["rank"].data
if current_user.rank.atLeast(newRank):
user.rank = form["rank"].data
else:
flash("Can't promote a user to a rank higher than yourself!", "danger")
if user.checkPerm(current_user, Permission.CHANGE_EMAIL):
newEmail = form["email"].data
if newEmail != user.email and newEmail.strip() != "":
token = randomString(32)
ver = UserEmailVerification()
ver.user = user
ver.token = token
ver.email = newEmail
db.session.add(ver)
db.session.commit()
task = sendVerifyEmail.delay(newEmail, token)
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=username)))
# Save user_profile
db.session.commit()
# Redirect to home page
return redirect(url_for("users.profile", username=username))
packages = user.packages.filter_by(soft_deleted=False)
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
packages = packages.filter_by(approved=True)
packages = packages.order_by(db.asc(Package.title))
topics_to_add = None
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
topics_to_add = ForumTopic.query \
.filter_by(author_id=user.id) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
pinned_collections = user.collections.filter(Collection.private == False,
Collection.pinned == True, Collection.packages.any()).all()
unlocked, locked = get_user_medals(user)
# Process GET or invalid POST
return render_template("users/profile.html",
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
return render_template("users/profile.html", user=user,
packages=packages, maintained_packages=maintained_packages,
medals_unlocked=unlocked, medals_locked=locked, pinned_collections=pinned_collections)
@bp.route("/users/<username>/check/", methods=["POST"])
@bp.route("/users/<username>/check-forums/", methods=["POST"])
@login_required
def user_check(username):
def user_check_forums(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
if current_user != user and not current_user.rank.atLeast(UserRank.MODERATOR):
if current_user != user and not current_user.rank.at_least(UserRank.MODERATOR):
abort(403)
if user.forums_username is None:
abort(404)
task = checkForumAccount.delay(user.forums_username)
task = check_forum_account.delay(user.forums_username, force_replace_pic=True)
next_url = url_for("users.profile", username=username)
return redirect(url_for("tasks.check", id=task.id, r=next_url))
class SendEmailForm(FlaskForm):
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
text = TextAreaField("Message", [InputRequired()])
submit = SubmitField("Send")
@bp.route("/users/<username>/remove-profile-pic/", methods=["POST"])
@login_required
def user_remove_profile_pic(username):
user = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.at_least(UserRank.MODERATOR):
abort(403)
user.profile_pic = None
db.session.commit()
return redirect(url_for("users.profile_edit", username=username))
@bp.route("/users/<username>/email/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def send_email(username):
@bp.route("/user/stats/")
@login_required
def statistics_redirect():
return redirect(url_for("users.statistics", username=current_user.username))
@bp.route("/users/<username>/stats/")
def statistics(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
next_url = url_for("users.profile", username=user.username)
downloads = db.session.query(func.sum(Package.downloads)).filter(Package.author==user).one()
if user.email is None:
flash("User has no email address!", "danger")
return redirect(next_url)
form = SendEmailForm(request.form)
if form.validate_on_submit():
text = form.text.data
html = render_markdown(text)
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
return redirect(url_for("tasks.check", id=task.id, r=next_url))
return render_template("users/send_email.html", form=form)
class SetPasswordForm(FlaskForm):
email = StringField("Email", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 100)])
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
submit = SubmitField("Save")
@bp.route("/user/set-password/", methods=["GET", "POST"])
@login_required
def set_password():
if current_user.hasPassword():
return redirect(url_for("user.change_password"))
form = SetPasswordForm(request.form)
if current_user.email == None:
form.email.validators = [InputRequired(), Email()]
if request.method == "POST" and form.validate():
one = form.password.data
two = form.password2.data
if one == two:
# Hash password
hashed_password = user_manager.hash_password(form.password.data)
# Change password
current_user.password = hashed_password
db.session.commit()
# Send 'password_changed' email
if user_manager.USER_ENABLE_EMAIL and current_user.email:
emails.send_password_changed_email(current_user)
# Send password_changed signal
signals.user_changed_password.send(current_app._get_current_object(), user=current_user)
# Prepare one-time system message
flash('Your password has been changed successfully.', 'success')
newEmail = form["email"].data
if newEmail != current_user.email and newEmail.strip() != "":
token = randomString(32)
ver = UserEmailVerification()
ver.user = current_user
ver.token = token
ver.email = newEmail
db.session.add(ver)
db.session.commit()
task = sendVerifyEmail.delay(newEmail, token)
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username)))
else:
return redirect(url_for("user.login"))
else:
flash("Passwords do not match", "danger")
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
@bp.route("/users/verify/")
def verify_email():
token = request.args.get("token")
ver = UserEmailVerification.query.filter_by(token=token).first()
if ver is None:
flash("Unknown verification token!", "danger")
else:
ver.user.email = ver.email
db.session.delete(ver)
db.session.commit()
if current_user.is_authenticated:
return redirect(url_for("users.profile", username=current_user.username))
else:
return redirect(url_for("homepage.home"))
start = request.args.get("start")
end = request.args.get("end")
return render_template("users/stats.html", user=user, downloads=downloads[0],
start=start, end=end, options=get_daterange_options(), noindex=start or end)

View File

@@ -0,0 +1,472 @@
# ContentDB
# Copyright (C) 2023 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 flask import redirect, abort, render_template, request, flash, url_for
from flask_babel import gettext, get_locale, lazy_gettext
from flask_login import current_user, login_required, logout_user
from flask_wtf import FlaskForm
from kombu import uuid
from sqlalchemy import or_
from wtforms import StringField, SubmitField, BooleanField, SelectField
from wtforms.validators import Length, Optional, Email, URL
from app.models import User, AuditSeverity, db, UserRank, PackageAlias, EmailSubscription, UserNotificationPreferences, \
UserEmailVerification, Permission, NotificationType, UserBan
from app.tasks.emails import send_verify_email
from app.tasks.usertasks import update_github_user_id
from app.utils import nonempty_or_none, add_audit_log, random_string, rank_required, has_blocked_domains
from . import bp
def get_setting_tabs(user):
ret = [
{
"id": "edit_profile",
"title": gettext("Edit Profile"),
"url": url_for("users.profile_edit", username=user.username)
},
{
"id": "account",
"title": gettext("Account and Security"),
"url": url_for("users.account", username=user.username)
},
{
"id": "notifications",
"title": gettext("Email and Notifications"),
"url": url_for("users.email_notifications", username=user.username)
},
{
"id": "api_tokens",
"title": gettext("API Tokens"),
"url": url_for("api.list_tokens", username=user.username)
},
]
if user.check_perm(current_user, Permission.CREATE_OAUTH_CLIENT):
ret.append({
"id": "oauth_clients",
"title": gettext("OAuth2 Applications"),
"url": url_for("oauth.list_clients", username=user.username)
})
if current_user.rank.at_least(UserRank.MODERATOR):
ret.append({
"id": "modtools",
"title": gettext("Moderator Tools"),
"url": url_for("users.modtools", username=user.username)
})
return ret
class UserProfileForm(FlaskForm):
display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[lambda x: nonempty_or_none(x.strip())])
website_url = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None])
donate_url = StringField(lazy_gettext("Donation URL"), [Optional(), URL()], filters = [lambda x: x or None])
submit = SubmitField(lazy_gettext("Save"))
def handle_profile_edit(form: UserProfileForm, user: User, username: str):
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
add_audit_log(severity, current_user, "Edited {}'s profile".format(user.display_name),
url_for("users.profile", username=username))
display_name = form.display_name.data or user.username
if user.check_perm(current_user, Permission.CHANGE_DISPLAY_NAME) and \
user.display_name != display_name:
if User.query.filter(User.id != user.id,
or_(User.username == display_name,
User.display_name.ilike(display_name))).count() > 0:
flash(gettext("A user already has that name"), "danger")
return None
alias_by_name = PackageAlias.query.filter(or_(
PackageAlias.author == display_name)).first()
if alias_by_name:
flash(gettext("A user already has that name"), "danger")
return
user.display_name = display_name
severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION
add_audit_log(severity, current_user, "Changed display name of {} to {}"
.format(user.username, user.display_name),
url_for("users.profile", username=username))
if user.check_perm(current_user, Permission.CHANGE_PROFILE_URLS):
if has_blocked_domains(form.website_url.data, current_user.username, f"{user.username}'s website_url") or \
has_blocked_domains(form.donate_url.data, current_user.username, f"{user.username}'s donate_url"):
flash(gettext("Linking to blocked sites is not allowed"), "danger")
return
user.website_url = form.website_url.data
user.donate_url = form.donate_url.data
db.session.commit()
return redirect(url_for("users.profile", username=username))
@bp.route("/users/<username>/settings/profile/", methods=["GET", "POST"])
@login_required
def profile_edit(username):
user : User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.can_see_edit_profile(current_user):
abort(403)
form = UserProfileForm(obj=user)
if form.validate_on_submit():
ret = handle_profile_edit(form, user, username)
if ret:
return ret
# Process GET or invalid POST
return render_template("users/profile_edit.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="edit_profile")
def make_settings_form():
attrs = {
"email": StringField(lazy_gettext("Email"), [Optional(), Email()]),
"submit": SubmitField(lazy_gettext("Save"))
}
for notificationType in NotificationType:
key = "pref_" + notificationType.to_name()
attrs[key] = BooleanField("")
attrs[key + "_digest"] = BooleanField("")
return type("SettingsForm", (FlaskForm,), attrs)
SettingsForm = make_settings_form()
def handle_email_notifications(user, prefs: UserNotificationPreferences, is_new, form):
for notificationType in NotificationType:
field_email = getattr(form, "pref_" + notificationType.to_name()).data
field_digest = getattr(form, "pref_" + notificationType.to_name() + "_digest").data or field_email
prefs.set_can_email(notificationType, field_email)
prefs.set_can_digest(notificationType, field_digest)
if is_new:
db.session.add(prefs)
if user.check_perm(current_user, Permission.CHANGE_EMAIL):
newEmail = form.email.data
if newEmail and newEmail != user.email and newEmail.strip() != "":
if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
flash(gettext("That email address has been unsubscribed/blacklisted, and cannot be used"), "danger")
return
token = random_string(32)
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
msg = "Changed email of {}".format(user.display_name)
add_audit_log(severity, current_user, msg, url_for("users.profile", username=user.username))
ver = UserEmailVerification()
ver.user = user
ver.token = token
ver.email = newEmail
db.session.add(ver)
db.session.commit()
send_verify_email.delay(newEmail, token, get_locale().language)
return redirect(url_for("users.email_sent"))
db.session.commit()
return redirect(url_for("users.email_notifications", username=user.username))
@bp.route("/user/settings/email/")
@bp.route("/users/<username>/settings/email/", methods=["GET", "POST"])
@login_required
def email_notifications(username=None):
if username is None:
return redirect(url_for("users.email_notifications", username=current_user.username))
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.check_perm(current_user, Permission.CHANGE_EMAIL):
abort(403)
is_new = False
prefs = user.notification_preferences
if prefs is None:
is_new = True
prefs = UserNotificationPreferences(user)
data = {}
types = []
for notificationType in NotificationType:
types.append(notificationType)
data["pref_" + notificationType.to_name()] = prefs.get_can_email(notificationType)
data["pref_" + notificationType.to_name() + "_digest"] = prefs.get_can_digest(notificationType)
data["email"] = user.email
form = SettingsForm(data=data)
if form.validate_on_submit():
ret = handle_email_notifications(user, prefs, is_new, form)
if ret:
return ret
return render_template("users/settings_email.html",
form=form, user=user, types=types, is_new=is_new,
tabs=get_setting_tabs(user), current_tab="notifications")
@bp.route("/users/<username>/settings/account/")
@login_required
def account(username):
user : User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.can_see_edit_profile(current_user):
abort(403)
return render_template("users/account.html", user=user, tabs=get_setting_tabs(user), current_tab="account")
@bp.route("/users/<username>/settings/account/disconnect-github/", methods=["POST"])
def disconnect_github(username: str):
user: User = User.query.filter_by(username=username).one_or_404()
if not user.can_see_edit_profile(current_user):
abort(403)
if user.password and user.email:
user.github_user_id = None
user.github_username = None
db.session.commit()
flash(gettext("Removed GitHub account"), "success")
else:
flash(gettext("You need to add an email address and password before you can remove your GitHub account"), "danger")
return redirect(url_for("users.account", username=username))
@bp.route("/users/<username>/delete/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def delete(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if user.rank.at_least(UserRank.MODERATOR):
flash(gettext("Users with moderator rank or above cannot be deleted"), "danger")
return redirect(url_for("users.account", username=username))
if request.method == "GET":
return render_template("users/delete.html", user=user, can_delete=user.can_delete())
if "delete" in request.form and (user.can_delete() or current_user.rank.at_least(UserRank.ADMIN)):
msg = "Deleted user {}".format(user.username)
flash(msg, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, msg, None)
if current_user.rank.at_least(UserRank.ADMIN):
for pkg in user.packages.all():
pkg.review_thread = None
db.session.delete(pkg)
db.session.delete(user)
elif "deactivate" in request.form:
for reply in user.replies.all():
db.session.delete(reply)
for thread in user.threads.all():
db.session.delete(thread)
for token in user.tokens.all():
db.session.delete(token)
user.profile_pic = None
user.email = None
if user.rank != UserRank.BANNED:
user.rank = UserRank.NOT_JOINED
msg = "Deactivated user {}".format(user.username)
flash(msg, "success")
add_audit_log(AuditSeverity.MODERATION, current_user, msg, None)
else:
assert False
db.session.commit()
if user == current_user:
logout_user()
return redirect(url_for("homepage.home"))
class ModToolsForm(FlaskForm):
username = StringField(lazy_gettext("Username"), [Optional(), Length(1, 50)])
display_name = StringField(lazy_gettext("Display name"), [Optional(), Length(2, 100)])
forums_username = StringField(lazy_gettext("Forums Username"), [Optional(), Length(2, 50)])
github_username = StringField(lazy_gettext("GitHub Username"), [Optional(), Length(2, 50)])
rank = SelectField(lazy_gettext("Rank"), [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce,
default=UserRank.NEW_MEMBER)
submit = SubmitField(lazy_gettext("Save"))
@bp.route("/users/<username>/modtools/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def modtools(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.check_perm(current_user, Permission.CHANGE_EMAIL):
abort(403)
form = ModToolsForm(obj=user)
if form.validate_on_submit():
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
add_audit_log(severity, current_user, "Edited {}'s account".format(user.display_name),
url_for("users.profile", username=username))
redirect_target = url_for("users.modtools", username=username)
# Copy form fields to user_profile fields
if user.check_perm(current_user, Permission.CHANGE_USERNAMES):
if user.username != form.username.data:
for package in user.packages:
alias = PackageAlias(user.username, package.name)
package.aliases.append(alias)
db.session.add(alias)
user.username = form.username.data
user.display_name = form.display_name.data
user.forums_username = nonempty_or_none(form.forums_username.data)
github_username = nonempty_or_none(form.github_username.data)
if github_username is None:
user.github_username = None
user.github_user_id = None
else:
task_id = uuid()
update_github_user_id.apply_async((user.id, github_username), task_id=task_id)
redirect_target = url_for("tasks.check", id=task_id, r=redirect_target)
if user.check_perm(current_user, Permission.CHANGE_RANK):
new_rank = form.rank.data
if current_user.rank.at_least(new_rank):
if new_rank != user.rank:
user.rank = form.rank.data
msg = "Set rank of {} to {}".format(user.display_name, user.rank.title)
add_audit_log(AuditSeverity.MODERATION, current_user, msg,
url_for("users.profile", username=username))
else:
flash(gettext("Can't promote a user to a rank higher than yourself!"), "danger")
db.session.commit()
return redirect(redirect_target)
return render_template("users/modtools.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="modtools")
@bp.route("/users/<username>/modtools/set-email/", methods=["POST"])
@rank_required(UserRank.MODERATOR)
def modtools_set_email(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.check_perm(current_user, Permission.CHANGE_EMAIL):
abort(403)
user.email = request.form["email"]
user.is_active = False
token = random_string(32)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Set email and sent a password reset on {user.username}",
url_for("users.profile", username=user.username), None)
ver = UserEmailVerification()
ver.user = user
ver.token = token
ver.email = user.email
ver.is_password_reset = True
db.session.add(ver)
db.session.commit()
send_verify_email.delay(user.email, token, user.locale or "en")
flash(f"Set email and sent a password reset on {user.username}", "success")
return redirect(url_for("users.modtools", username=username))
@bp.route("/users/<username>/modtools/ban/", methods=["POST"])
@rank_required(UserRank.MODERATOR)
def modtools_ban(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.check_perm(current_user, Permission.CHANGE_RANK):
abort(403)
message = request.form["message"]
expires_at = request.form.get("expires_at")
user.ban = UserBan()
user.ban.banned_by = current_user
user.ban.message = message
if expires_at and expires_at != "":
user.ban.expires_at = expires_at
else:
user.rank = UserRank.BANNED
add_audit_log(AuditSeverity.MODERATION, current_user, f"Banned {user.username}, expires {user.ban.expires_at or '-'}, message: {message}",
url_for("users.profile", username=user.username), None)
db.session.commit()
flash(f"Banned {user.username}", "success")
return redirect(url_for("users.modtools", username=username))
@bp.route("/users/<username>/modtools/unban/", methods=["POST"])
@rank_required(UserRank.MODERATOR)
def modtools_unban(username):
user: User = User.query.filter_by(username=username).first()
if not user:
abort(404)
if not user.check_perm(current_user, Permission.CHANGE_RANK):
abort(403)
if user.ban:
db.session.delete(user.ban)
if user.rank == UserRank.BANNED:
user.rank = UserRank.MEMBER
add_audit_log(AuditSeverity.MODERATION, current_user, f"Unbanned {user.username}",
url_for("users.profile", username=user.username), None)
db.session.commit()
flash(f"Unbanned {user.username}", "success")
return redirect(url_for("users.modtools", username=username))

View File

@@ -0,0 +1,22 @@
# ContentDB
# Copyright (C) 2024 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 flask import Blueprint
bp = Blueprint("vcs", __name__)
from . import github, gitlab

View File

@@ -0,0 +1,43 @@
# ContentDB
# Copyright (C) 2024 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 app.blueprints.api.support import error
from app.models import Package, APIToken, Permission, PackageState
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
repo_url = repo_url.replace("https://", "").replace("http://", "").lower()
if token.package:
packages = [token.package]
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):
return error(403, "You do not have the permission to approve releases")
actual_repo_url: str = token.package.repo or ""
if repo_url not in actual_repo_url.lower():
return error(400, "Repo URL does not match the API token's package")
else:
# Get package
packages = Package.query.filter(
Package.repo.ilike("%{}%".format(repo_url)), Package.state != PackageState.DELETED).all()
if len(packages) == 0:
return error(400,
"Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(repo_url))
packages = [x for x in packages if x.check_perm(token.owner, Permission.APPROVE_RELEASE)]
if len(packages) == 0:
return error(403, "You do not have the permission to approve releases")
return packages

View File

@@ -0,0 +1,200 @@
# ContentDB
# Copyright (C) 2018-24 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 datetime
import hmac
import requests
from flask import abort, Response
from flask import redirect, url_for, request, flash, jsonify, current_app
from flask_babel import gettext
from flask_login import current_user
from app import github, csrf
from app.blueprints.api.support import error, api_create_vcs_release
from app.logic.users import create_user
from app.models import db, User, APIToken, AuditSeverity
from app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
from . import bp
from .common import get_packages_for_vcs_and_token
@bp.route("/github/start/")
def github_start():
next = request.args.get("next")
if next and not is_safe_url(next):
abort(400)
return github.authorize("", redirect_uri=abs_url_for("vcs.github_callback", next=next))
@bp.route("/github/view/")
def github_view_permissions():
url = "https://github.com/settings/connections/applications/" + \
current_app.config["GITHUB_CLIENT_ID"]
return redirect(url)
@bp.route("/github/callback/")
@github.authorized_handler
def github_callback(oauth_token):
if oauth_token is None:
flash(gettext("Authorization failed [err=gh-oauth-login-failed]"), "danger")
return redirect(url_for("users.login"))
next = request.args.get("next")
if next and not is_safe_url(next):
abort(400)
redirect_to = next
if redirect_to is None:
redirect_to = url_for("homepage.home")
# Get GitGub username
url = "https://api.github.com/user"
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
json = r.json()
user_id = json["id"]
github_username = json["login"]
if type(user_id) is not int:
abort(400)
# Get user by GitHub user ID
user_by_github = User.query.filter(User.github_user_id == user_id).one_or_none()
# If logged in, connect
if current_user and current_user.is_authenticated:
if user_by_github is None:
current_user.github_username = github_username
current_user.github_user_id = user_id
db.session.commit()
flash(gettext("Linked GitHub to account"), "success")
return redirect(redirect_to)
elif user_by_github == current_user:
return redirect(redirect_to)
else:
flash(gettext("GitHub account is already associated with another user: %(username)s",
username=user_by_github.username), "danger")
return redirect(redirect_to)
# Log in to existing account
elif user_by_github:
ret = login_user_set_active(user_by_github, next, remember=True)
if ret is None:
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
return redirect(url_for("users.login"))
user_by_github.github_username = github_username
add_audit_log(AuditSeverity.USER, user_by_github, "Logged in using GitHub OAuth",
url_for("users.profile", username=user_by_github.username))
db.session.commit()
return ret
# Sign up
else:
user = create_user(github_username, github_username, None, "GitHub")
if isinstance(user, Response):
return user
elif user is None:
return redirect(url_for("users.login"))
user.github_username = github_username
user.github_user_id = user_id
add_audit_log(AuditSeverity.USER, user, "Registered with GitHub, display name=" + user.display_name,
url_for("users.profile", username=user.username))
db.session.commit()
ret = login_user_set_active(user, next, remember=True)
if ret is None:
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
return redirect(url_for("users.login"))
return ret
def _find_api_token(header_signature: str) -> APIToken:
sha_name, signature = header_signature.split('=')
if sha_name != 'sha1':
error(403, "Expected SHA1 payload signature")
for token in APIToken.query.all():
mac = hmac.new(token.access_token.encode("utf-8"), msg=request.data, digestmod='sha1')
if hmac.compare_digest(str(mac.hexdigest()), signature):
return token
error(401, "Invalid authentication, couldn't validate API token")
@bp.route("/github/webhook/", methods=["POST"])
@csrf.exempt
def github_webhook():
json = request.json
header_signature = request.headers.get('X-Hub-Signature')
if header_signature is None:
return error(403, "Expected payload signature")
token = _find_api_token(header_signature)
packages = get_packages_for_vcs_and_token(token, "github.com/" + json["repository"]["full_name"])
for package in packages:
#
# Check event
#
event = request.headers.get("X-GitHub-Event")
if event == "push":
ref = json["after"]
title = datetime.datetime.utcnow().strftime("%Y-%m-%d") + " " + ref[:5]
branch = json["ref"].replace("refs/heads/", "")
if package.update_config and package.update_config.ref:
if branch != package.update_config.ref:
continue
elif branch not in ["master", "main"]:
continue
elif event == "create":
ref_type = json.get("ref_type")
if ref_type != "tag":
return jsonify({
"success": False,
"message": "Webhook ignored, as it's a non-tag create event. ref_type='{}'.".format(ref_type)
})
ref = json["ref"]
title = ref
elif event == "ping":
return jsonify({"success": True, "message": "Ping successful"})
else:
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
.format(event or "null"))
#
# Perform release
#
if package.releases.filter_by(commit_hash=ref).count() > 0:
return
return api_create_vcs_release(token, package, title, title, None, ref, reason="Webhook")
return jsonify({
"success": False,
"message": "No release made. Either the release already exists or the event was filtered based on the branch"
})

View File

@@ -0,0 +1,86 @@
# ContentDB
# Copyright (C) 2020-24 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 datetime
from flask import request, jsonify
from app import csrf
from app.blueprints.api.support import error, api_create_vcs_release
from app.models import APIToken
from . import bp
from .common import get_packages_for_vcs_and_token
def webhook_impl():
json = request.json
# Get all tokens for package
secret = request.headers.get("X-Gitlab-Token")
if secret is None:
return error(403, "Token required")
token: APIToken = APIToken.query.filter_by(access_token=secret).first()
if token is None:
return error(403, "Invalid authentication")
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"])
for package in packages:
#
# Check event
#
event = json["event_name"]
if event == "push":
ref = json["after"]
title = datetime.datetime.utcnow().strftime("%Y-%m-%d") + " " + ref[:5]
branch = json["ref"].replace("refs/heads/", "")
if package.update_config and package.update_config.ref:
if branch != package.update_config.ref:
continue
elif branch not in ["master", "main"]:
continue
elif event == "tag_push":
ref = json["ref"]
title = ref.replace("refs/tags/", "")
else:
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
.format(event or "null"))
#
# Perform release
#
if package.releases.filter_by(commit_hash=ref).count() > 0:
continue
return api_create_vcs_release(token, package, title, title, None, ref, reason="Webhook")
return jsonify({
"success": False,
"message": "No release made. Either the release already exists or the event was filtered based on the branch"
})
@bp.route("/gitlab/webhook/", methods=["POST"])
@csrf.exempt
def gitlab_webhook():
try:
return webhook_impl()
except KeyError as err:
return error(400, "Missing field: {}".format(err.args[0]))

View File

@@ -0,0 +1,70 @@
# ContentDB
# Copyright (C) 2022 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 celery import uuid
from flask import Blueprint, render_template, redirect, request, abort, url_for
from flask_babel import lazy_gettext
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField, SelectMultipleField
from wtforms.validators import InputRequired, Length, Optional
from app.tasks import celery
from app.utils import rank_required
bp = Blueprint("zipgrep", __name__)
from app.models import UserRank, Package, PackageType
from app.tasks.zipgrep import search_in_releases
class SearchForm(FlaskForm):
query = StringField(lazy_gettext("Text to find (regex)"), [InputRequired(), Length(1, 100)])
file_filter = StringField(lazy_gettext("File filter"), [InputRequired(), Length(1, 100)], default="*.lua")
type = SelectMultipleField(lazy_gettext("Type"), [Optional()],
choices=PackageType.choices(), coerce=PackageType.coerce)
submit = SubmitField(lazy_gettext("Search"))
@bp.route("/zipgrep/", methods=["GET", "POST"])
@rank_required(UserRank.EDITOR)
def zipgrep_search():
form = SearchForm(request.form)
if form.validate_on_submit():
task_id = uuid()
search_in_releases.apply_async((form.query.data, form.file_filter.data, [x.name for x in form.type.data]), task_id=task_id)
result_url = url_for("zipgrep.view_results", id=task_id)
return redirect(url_for("tasks.check", id=task_id, r=result_url))
return render_template("zipgrep/search.html", form=form)
@bp.route("/zipgrep/<id>/")
def view_results(id):
result = celery.AsyncResult(id)
if result.status == "PENDING":
abort(404)
if result.status != "SUCCESS" or isinstance(result.result, Exception):
result_url = url_for("zipgrep.view_results", id=id)
return redirect(url_for("tasks.check", id=id, r=result_url))
matches = result.result["matches"]
for match in matches:
match["package"] = Package.query.filter(
Package.name == match["package"]["name"],
Package.author.has(username=match["package"]["author"])).one()
return render_template("zipgrep/view_results.html", query=result.result["query"], matches=matches)

View File

@@ -1,25 +1,51 @@
from .models import *
from .utils import make_flask_user_password
# 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 datetime
from .models import User, UserRank, LuantiRelease, Tag, License, Notification, NotificationType, Package, \
PackageState, PackageType, PackageRelease, MetaPackage, Dependency
from .utils import make_flask_login_password
def populate(session):
admin_user = User("rubenwardy")
admin_user.active = True
admin_user.password = make_flask_user_password("tuckfrump")
admin_user.is_active = True
admin_user.password = make_flask_login_password("tuckfrump")
admin_user.github_username = "rubenwardy"
admin_user.forums_username = "rubenwardy"
admin_user.rank = UserRank.ADMIN
session.add(admin_user)
session.add(MinetestRelease("None", 0))
session.add(MinetestRelease("0.4.16/17", 32))
session.add(MinetestRelease("5.0", 37))
session.add(MinetestRelease("5.1", 38))
system_user = User("ContentDB", active=False)
system_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
system_user.rank = UserRank.BOT
session.add(system_user)
session.add(LuantiRelease("None", 0))
session.add(LuantiRelease("0.4.16/17", 32))
session.add(LuantiRelease("5.0", 37))
session.add(LuantiRelease("5.1", 38))
session.add(LuantiRelease("5.2", 39))
session.add(LuantiRelease("5.3", 39))
tags = {}
for tag in ["Inventory", "Mapgen", "Building", \
"Mobs and NPCs", "Tools", "Player effects", \
"Environment", "Transport", "Maintenance", "Plants and farming", \
for tag in ["Inventory", "Mapgen", "Building",
"Mobs and NPCs", "Tools", "Player effects",
"Environment", "Transport", "Maintenance", "Plants and farming",
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer"]:
row = Tag(tag)
tags[row.name] = row
@@ -27,8 +53,8 @@ def populate(session):
licenses = {}
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
row = License(license)
licenses[row.name] = row
session.add(row)
@@ -43,9 +69,8 @@ def populate_test_data(session):
licenses = { x.name : x for x in License.query.all() }
tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
v4 = MinetestRelease.query.filter_by(protocol=32).first()
v50 = MinetestRelease.query.filter_by(protocol=37).first()
v51 = MinetestRelease.query.filter_by(protocol=38).first()
v4 = LuantiRelease.query.filter_by(protocol=32).first()
v51 = LuantiRelease.query.filter_by(protocol=38).first()
ez = User("Shara")
ez.github_username = "Ezhh"
@@ -53,7 +78,7 @@ def populate_test_data(session):
ez.rank = UserRank.EDITOR
session.add(ez)
not1 = Notification(admin_user, ez, "Awards approved", "/packages/rubenwardy/awards/")
not1 = Notification(admin_user, ez, NotificationType.PACKAGE_APPROVAL, "Awards approved", "/packages/rubenwardy/awards/")
session.add(not1)
jeija = User("Jeija")
@@ -61,9 +86,8 @@ def populate_test_data(session):
jeija.forums_username = "Jeija"
session.add(jeija)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "alpha"
mod.title = "Alpha Test"
mod.license = licenses["MIT"]
@@ -81,13 +105,14 @@ def populate_test_data(session):
rel = PackageRelease()
rel.package = mod
rel.name = "v1.0.0"
rel.title = "v1.0.0"
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
rel.approved = True
session.add(rel)
mod1 = Package()
mod1.approved = True
mod1.state = PackageState.APPROVED
mod1.name = "awards"
mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"]
@@ -102,7 +127,7 @@ def populate_test_data(session):
mod1.desc = """
Majority of awards are back ported from Calinou's old fork in Carbone, under same license.
```
```lua
awards.register_achievement("award_mesefind",{
title = "First Mese Find",
description = "Found some Mese!",
@@ -118,13 +143,14 @@ awards.register_achievement("award_mesefind",{
rel = PackageRelease()
rel.package = mod1
rel.min_rel = v51
rel.name = "v1.0.0"
rel.title = "v1.0.0"
rel.url = "https://github.com/rubenwardy/awards/archive/master.zip"
rel.approved = True
session.add(rel)
mod2 = Package()
mod2.approved = True
mod2.state = PackageState.APPROVED
mod2.name = "mesecons"
mod2.title = "Mesecons"
mod2.tags.append(tags["tools"])
@@ -213,7 +239,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod2)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "handholds"
mod.title = "Handholds"
mod.license = licenses["MIT"]
@@ -230,6 +256,7 @@ No warranty is provided, express or implied, for any part of the project.
rel = PackageRelease()
rel.package = mod
rel.name = "v1.0.0"
rel.title = "v1.0.0"
rel.max_rel = v4
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
@@ -237,7 +264,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(rel)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "other_worlds"
mod.title = "Other Worlds"
mod.license = licenses["MIT"]
@@ -254,7 +281,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "food"
mod.title = "Food"
mod.license = licenses["LGPLv2.1"]
@@ -270,7 +297,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "food_sweet"
mod.title = "Sweet Foods"
mod.license = licenses["CC0"]
@@ -287,7 +314,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
game1 = Package()
game1.approved = True
game1.state = PackageState.APPROVED
game1.name = "capturetheflag"
game1.title = "Capture The Flag"
game1.type = PackageType.GAME
@@ -343,6 +370,7 @@ Uses the CTF PvP Engine.
rel = PackageRelease()
rel.package = game1
rel.name = "v1.0.0"
rel.title = "v1.0.0"
rel.url = "https://github.com/rubenwardy/capturetheflag/archive/master.zip"
rel.approved = True
@@ -350,11 +378,11 @@ Uses the CTF PvP Engine.
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "pixelbox"
mod.title = "PixelBOX Reloaded"
mod.license = licenses["CC0"]
mod.media_license = licenses["MIT"]
mod.media_license = licenses["CC0"]
mod.type = PackageType.TXP
mod.author = admin_user
mod.forums = 14132
@@ -364,6 +392,7 @@ Uses the CTF PvP Engine.
rel = PackageRelease()
rel.package = mod
rel.name = "v1.0.0"
rel.title = "v1.0.0"
rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip"
rel.approved = True
@@ -373,7 +402,6 @@ Uses the CTF PvP Engine.
metas = {}
for package in Package.query.filter_by(type=PackageType.MOD).all():
meta = None
try:
meta = metas[package.name]
except KeyError:

49
app/flatpages/about.md Normal file
View File

@@ -0,0 +1,49 @@
title: About ContentDB
description: Information about ContentDB's development, history, and more
toc: False
## Development
ContentDB was created by [rubenwardy](https://rubenwardy.com/) in 2018, he was lucky enough to have the time available
as it was submitted as university coursework. To learn about the history and development of ContentDB,
[see the blog post](https://blog.rubenwardy.com/2022/03/24/contentdb/).
ContentDB is open source software, licensed under AGPLv3.0.
<a href="https://github.com/luanti-org/contentdb/" class="btn btn-primary me-1">Source code</a>
<a href="https://github.com/luanti-org/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
{% if monitoring_url -%}
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a>
{%- endif %}
## Why was ContentDB created?
Before ContentDB, users had to manually install mods and games by unzipping their files into a directory. This is
poor user experience, especially for first-time users.
ContentDB isn't just about supporting the in-game content downloader; it's common for technical users to find
and review packages using the ContentDB website, but install using Git rather than the in-game installer.
**ContentDB's purpose is to be a well-formatted source of information about mods, games,
and texture packs for Luanti**.
## How do I learn how to make mods and games for Luanti?
You should read
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
<h2 id="donate">How can I support / donate to ContentDB?</h2>
You can donate to rubenwardy to cover ContentDB's costs and support future development.
For more information about the cost of ContentDB and what rubenwardy does, see his donation page:
<a href="https://rubenwardy.com/donate/" class="btn btn-primary me-1">Donate</a>
<a href="/donate/" class="btn btn-secondary">Support Creators</a>
## Sponsorships
Luanti and ContentDB are sponsored by <a href="https://sentry.io/" rel="nofollow">sentry.io</a>.
This provides us with improved error logging and performance insights.

View File

@@ -1,8 +1,42 @@
title: Help
toc: False
* [Package Tags](package_tags)
* [Ranks and Permissions](ranks_permissions)
* [Content Ratings and Flags](content_flags)
* [Reporting Content](reporting)
* [API](api)
* [Creating Releases using Webhooks](release_webhooks)
## Rules
* [Terms of Service](/terms/)
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
## General Help
* [Frequently Asked Questions](faq/)
* [Installing content](installing/)
* [Content Ratings and Flags](content_flags/)
* [Non-free Licenses](non_free/)
* [Why WTFPL is a terrible license](wtfpl/)
* [Ranks and Permissions](ranks_permissions/)
* [Contact Us](contact_us/)
* [Top Packages Algorithm](top_packages/)
* [Featured Packages](featured/)
* [Feeds](feeds/)
## Help for Package Authors
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
* [Copyright Guide](copyright/)
* [Git Update Detection](update_config/)
* [Creating Releases using Webhooks](release_webhooks/)
* [Package Configuration and Releases Guide](package_config/)
* [Supported Games](game_support/)
* [Creating an appealing ContentDB page](appealing_page/)
## Help for Specific User Ranks
* [Editors](editors/)
## APIs
* [API](api/)
* [OAuth2 Applications](oauth/)
* [Prometheus Metrics](metrics/)

View File

@@ -1,64 +1,592 @@
title: API
## Resources
* [How the Luanti client uses the API](https://github.com/luanti-org/contentdb/blob/master/docs/luanti_client.md)
## Responses and Error Handling
If there is an error, the response will be JSON similar to the following with a non-200 status code:
```json
{
"success": false,
"error": "The error message"
}
```
Successful GET requests will return the resource's information directly as a JSON response.
Other successful results will return a dictionary with `success` equaling true, and
often other keys with information. For example:
```js
{
"success": true,
"release": {
/* same as returned by a GET */
}
}
```
### Paginated Results
Some API endpoints returns results in pages. The page number is specified using the `page` query argument, and
the number of items is specified using `num`
The response will be a dictionary with the following keys:
* `page`: page number, integer from 1 to max
* `per_page`: number of items per page, same as `n`
* `page_count`: number of pages
* `total`: total number of results
* `urls`: dictionary containing
* `next`: url to next page
* `previous`: url to previous page
* `items`: array of items
## Authentication
Not all endpoints require authentication.
Authentication is done using Bearer tokens:
Not all endpoints require authentication, but it is done using Bearer tokens:
Authorization: Bearer YOURTOKEN
```bash
curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN"
```
You can use the `/api/whoami` to check authentication.
Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
## Endpoints
* GET `/api/whoami/`: JSON dictionary with the following keys:
* `is_authenticated`: True on successful API authentication
* `username`: Username of the user authenticated as, null otherwise.
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
* DELETE `/api/delete-token/`: Deletes the currently used token.
### Misc
* GET `/api/whoami/` - Json dictionary with the following keys:
* `is_authenticated` - True on successful API authentication
* `username` - Username of the user authenticated as, null otherwise.
* 403 will be thrown on unsupported authentication type, invalid access token, or other errors.
### Packages
* GET `/api/packages/` - See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/`
### Releases
* GET `/api/packages/<username>/<name>/releases/`
* POST `/api/packages/<username>/<name>/releases/new/`
* Requires authentication.
* `title`: human-readable name of the release.
* `method`: Release-creation method, only `git` is supported.
* `min_protocol`: (Optional) minimum Minetest protocol version. See [Minetest](#minetest).
* `min_protocol`: (Optional) maximum Minetest protocol version. See [Minetest](#minetest).
* If `git` release-creation method:
* `ref` - git reference, eg: `master`.
```bash
# Logout
curl -X DELETE https://content.luanti.org/api/delete-token/ \
-H "Authorization: Bearer YOURTOKEN"
```
### Topics
## Packages
* GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options:
* `show_added` - Show topics which exist as packages, default true.
* `show_discarded` - Show topics which have been marked as outdated, default false.
* GET `/api/packages/` (List)
* See [Package Queries](#package-queries)
* GET `/api/packages/<username>/<name>/` (Read)
* Redirects a JSON object with the keys documented by the PUT endpoint, below.
* Plus:
* `forum_url`: String or null.
* PUT `/api/packages/<author>/<name>/` (Update)
* Requires authentication.
* JSON object with any of these keys (all are optional, null to delete Nullables):
* `type`: One of `GAME`, `MOD`, `TXP`.
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
* `short_description`
* `dev_state`: One of `WIP`, `BETA`, `ACTIVELY_DEVELOPED`, `MAINTENANCE_ONLY`, `AS_IS`, `DEPRECATED`,
`LOOKING_FOR_MAINTAINER`.
* `tags`: List of [tag](#tags) names.
* `content_warnings`: List of [content warning](#content-warnings) names.
* `license`: A [license](#licenses) name.
* `media_license`: A [license](#licenses) name.
* `long_description`: Long markdown description.
* `repo`: Source repository (eg: Git)
* `website`: Website URL.
* `issue_tracker`: Issue tracker URL.
* `forums`: forum topic ID.
* `video_url`: URL to a video.
* `donate_url`: URL to a donation page.
* `translation_url`: URL to send users interested in translating your package.
* `game_support`: Array of game support information objects. Not currently documented,
* Returns a JSON object with:
* `success`
* `package`: updated package
* `was_modified`: bool, whether anything changed
* GET `/api/packages/<username>/<name>/for-client/`
* Similar to the read endpoint, but optimised for the Luanti client
* `long_description` is given as a hypertext object, see `/hypertext/` below.
* `info_hypertext` is the info sidebar as a hypertext object.
* Query arguments
* `formspec_version`: Required. See /hypertext/ below.
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
* `protocol_version`: Optional, used to get the correct release.
* `engine_version`: Optional, used to get the correct release. Ex: `5.3.0`.
* GET `/api/packages/<username>/<name>/for-client/reviews/`
* Returns hypertext representing the package's reviews
* Query arguments
* `formspec_version`: Required. See /hypertext/ below.
* Returns JSON dictionary with following keys:
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
* `body`: markup for long description.
* `links`: dictionary of anchor name to link URL.
* `images`: dictionary of img name to image URL.
* `image_tooltips`: dictionary of img name to tooltip text.
* The hypertext body contains some placeholders that should be replaced client-side:
* `<thumbsup>` with a thumbs up icon.
* `<neutral>` with a thumbs up icon.
* `<thumbsdown>` with a thumbs up icon.
* GET `/api/packages/<author>/<name>/hypertext/`
* Converts the long description to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* Query arguments:
* `formspec_version`: Required, maximum supported formspec version.
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
* Returns JSON dictionary with following 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.
* GET `/api/packages/<username>/<name>/dependencies/`
* Returns dependencies, with suggested candidates
* If query argument `only_hard` is present, only hard deps will be returned.
* GET `/api/dependencies/`
* Returns `provides` and raw dependencies for all packages.
* Supports [Package Queries](#package-queries)
* [Paginated result](#paginated-results), max 300 results per page
* Each item in `items` will be a dictionary with the following keys:
* `type`: One of `GAME`, `MOD`, `TXP`.
* `author`: Username of the package author.
* `name`: Package name.
* `provides`: List of technical mod names inside the package.
* `depends`: List of hard dependencies.
* Each dep will either be a modname dependency (`name`), or a
package dependency (`author/name`).
* `optional_depends`: list of optional dependencies
* Same as above.
* GET `/api/packages/<username>/<name>/stats/`
* Returns daily stats for package, or null if there is no data.
* Daily date is done based on the UTC timezone.
* EXPERIMENTAL. This API may change without warning.
* Query args:
* `start`: start date, inclusive. Optional. Default: 2022-10-01. UTC.
* `end`: end date, inclusive. Optional. Default: today. UTC.
* An object with the following keys:
* `start`: start date, inclusive. Ex: 2022-10-22. M
* `end`: end date, inclusive. Ex: 2022-11-05.
* `platform_minetest`: list of integers per day.
* `platform_other`: list of integers per day.
* `reason_new`: list of integers per day.
* `reason_dependency`: list of integers per day.
* `reason_update`: list of integers per day.
* GET `/api/package_stats/`
* Returns last 30 days of daily stats for _all_ packages.
* An object with the following keys:
* `start`: start date, inclusive. Ex: 2022-10-22.
* `end`: end date, inclusive. Ex: 2022-11-05.
* `package_downloads`: map from package key to list of download integers.
### Minetest
You can download a package by building one of the two URLs:
* GET `/api/minetest_versions/`
```
https://content.luanti.org/packages/${author}/${name}/download/`
https://content.luanti.org/packages/${author}/${name}/releases/${release}/download/`
```
Examples:
## Package Queries
```bash
# Edit package
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }'
# Remove website URL
curl -X PUT https://content.luanti.org/api/packages/username/name/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{ "website": null }'
```
### Package Queries
Example:
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
Filter query parameters:
* `type`: Filter by package type (`mod`, `game`, `txp`). Multiple types are OR-ed together.
* `q`: Query string.
* `author`: Filter by author.
* `tag`: Filter by tags. Multiple tags are AND-ed together.
* `flag`: Filter to show packages with [Content Flags](/help/content_flags/).
* `hide`: Hide content based on tags or [Content Flags](/help/content_flags/).
* `license`: Filter by [license name](#licenses). Multiple licenses are OR-ed together, ie: `&license=MIT&license=LGPL-2.1-only`
* `game`: Filter by [Game Support](/help/game_support/), ex: `Warr1024/nodecore`. (experimental, doesn't show items that support every game currently).
* `lang`: Filter by translation support, eg: `en`/`de`/`ja`/`zh_TW`.
* `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
Sorting query parameters:
* `sort`: Sort by (`name`, `title`, `score`, `reviews`, `downloads`, `created_at`, `approved_at`, `last_release`).
* `order`: Sort ascending (`asc`) or descending (`desc`).
* `random`: When present, enable random ordering and ignore `sort`.
Format query parameters:
* `limit`: Return at most `limit` packages.
* `fmt`: How the response is formatted.
* `keys`: author/name only.
* `short`: stuff needed for the Luanti client.
* `vcs`: `short` but with `repo`.
### Releases
* GET `/api/releases/` (List)
* Limited to 30 most recent releases.
* Optional arguments:
* `author`: Filter by author
* `maintainer`: Filter by maintainer
* Returns array of release dictionaries with keys:
* `id`: release ID
* `name`: short release name
* `title`: human-readable title
* `release_notes`: string or null, what's new in this release. Markdown.
* `release_date`: Date released
* `url`: download URL
* `commit`: commit hash or null
* `downloads`: number of downloads
* `min_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `max_minetest_version`: dict or null, minimum supported Luanti version (inclusive).
* `size`: size of zip file, in bytes.
* `package`
* `author`: author username
* `name`: technical name
* `type`: `mod`, `game`, or `txp`
* GET `/api/updates/` (Look-up table)
* Returns a look-up table from package key (`author/name`) to latest release id
* Query arguments
* `protocol_version`: Only show packages supported by this Luanti protocol version.
* `engine_version`: Only show packages supported by this Luanti engine version, eg: `5.3.0`.
* GET `/api/packages/<username>/<name>/releases/` (List)
* Returns array of release dictionaries, see above, but without package info.
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
* Requires authentication.
* Body can be JSON or multipart form data. Zip uploads must be multipart form data.
* `title`: human-readable name of the release.
* `release_notes`: string or null, what's new in this release.
* For Git release creation:
* `method`: must be `git`.
* `ref`: (Optional) git reference, eg: `master`.
* For zip upload release creation:
* `file`: multipart file to upload, like `<input type="file" name="file">`.
* `commit`: (Optional) Source Git commit hash, for informational purposes.
* You can set min and max Luanti Versions [using the content's .conf file](/help/package_config/).
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
* Requires authentication.
* Deletes release.
Examples:
```bash
# Create release from Git
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d '{
"method": "git",
"name": "1.2.3",
"title": "My Release",
"ref": "master",
"release_notes": "some\nrelease\nnotes\n"
}'
# Create release from zip upload
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip
# Create release from zip upload with commit hash
curl -X POST https://content.luanti.org/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F commit="8ef74deec170a8ce789f6055a59d43876d16a7ea" -F file=@path/to/file.zip
# Delete release
curl -X DELETE https://content.luanti.org/api/packages/username/name/releases/3/ \
-H "Authorization: Bearer YOURTOKEN"
```
### Screenshots
* GET `/api/packages/<username>/<name>/screenshots/` (List)
* Returns array of screenshot dictionaries with keys:
* `id`: screenshot ID
* `approved`: true if approved and visible.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `url`: absolute URL to screenshot.
* `created_at`: ISO time.
* `order`: Number used in ordering.
* `is_cover_image`: true for cover image.
* GET `/api/packages/<username>/<name>/screenshots/<id>/` (Read)
* Returns screenshot dictionary like above.
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
* Requires authentication.
* Body is multipart form data.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `file`: multipart file to upload, like `<input type=file>`.
* `is_cover_image`: set cover image to this.
* DELETE `/api/packages/<username>/<name>/screenshots/<id>/` (Delete)
* Requires authentication.
* Deletes screenshot.
* POST `/api/packages/<username>/<name>/screenshots/order/`
* Requires authentication.
* Body is a JSON array containing the screenshot IDs in their order.
* POST `/api/packages/<username>/<name>/screenshots/cover-image/`
* Requires authentication.
* Body is a JSON dictionary with "cover_image" containing the screenshot ID.
Currently, to get a different size of thumbnail you can replace the number in `/thumbnails/1/` with any number from 1-3.
The resolutions returned may change in the future, and we may move to a more capable thumbnail generation.
Examples:
```bash
# Create screenshot
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png
# Create screenshot and set it as the cover image
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
# Delete screenshot
curl -X DELETE https://content.luanti.org/api/packages/username/name/screenshots/3/ \
-H "Authorization: Bearer YOURTOKEN"
# Reorder screenshots
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/order/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]"
# Set cover image
curl -X POST https://content.luanti.org/api/packages/username/name/screenshots/cover-image/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{ 'cover_image': 123 }"
```
### Reviews
* GET `/api/packages/<username>/<name>/reviews/` (List)
* Returns array of review dictionaries with keys:
* `user`: dictionary with `display_name` and `username`.
* `title`: review title
* `comment`: the text
* `rating`: 1 for negative, 3 for neutral, 5 for positive
* `is_positive`: boolean
* `created_at`: iso timestamp
* `votes`: dictionary with `helpful` and `unhelpful`,
* GET `/api/reviews/` (List)
* Returns a paginated response. This is a dictionary with `page`, `url`, and `items`.
* [Paginated result](#paginated-results)
* `items`: array of review dictionaries, like above
* Each review also has a `package` dictionary with `type`, `author` and `name`
* Ordered by created at, newest to oldest.
* Query arguments:
* `page`: page number, integer from 1 to max
* `n`: number of results per page, max 200
* `author`: filter by review author username
* `for_user`: filter by package author
* `rating`: 1 for negative, 3 for neutral, 5 for positive
* `is_positive`: true or false. Default: null
* `q`: filter by title (case-insensitive, no fulltext search)
Example:
```json
[
{
"comment": "This is a really good mod!",
"created_at": "2021-11-24T16:18:33.764084",
"is_positive": true,
"title": "Really good",
"user": {
"display_name": "rubenwardy",
"username": "rubenwardy"
},
"votes": {
"helpful": 0,
"unhelpful": 0
}
}
]
```
## Users
* GET `/api/users/<username>/`
* `username`
* `display_name`: human-readable name to be displayed in GUIs.
* `rank`: ContentDB [rank](/help/ranks_permissions/).
* `profile_pic_url`: URL to profile picture, or null.
* `website_url`: URL to website, or null.
* `donate_url`: URL to donate page, or null.
* `connections`: object
* `github`: GitHub username, or null.
* `forums`: forums username, or null.
* `links`: object
* `api_packages`: URL to API to list this user's packages.
* `profile`: URL to the HTML profile page.
* GET `/api/users/<username>/stats/`
* Returns daily stats for the user's packages, or null if there is no data.
* Daily date is done based on the UTC timezone.
* EXPERIMENTAL. This API may change without warning.
* Query args:
* `start`: start date, inclusive. Optional. Default: 2022-10-01. UTC.
* `end`: end date, inclusive. Optional. Default: today. UTC.
* A table with the following keys:
* `from`: start date, inclusive. Ex: 2022-10-22.
* `end`: end date, inclusive. Ex: 2022-11-05.
* `package_downloads`: map of package title to list of integers per day.
* `platform_minetest`: list of integers per day.
* `platform_other`: list of integers per day.
* `reason_new`: list of integers per day.
* `reason_dependency`: list of integers per day.
* `reason_update`: list of integers per day.
## Topics
* GET `/api/topics/` ([View](/api/topics/))
* See [Topic Queries](#topic-queries)
### Topic Queries
Example:
/api/topics/?q=mobs&type=mod&type=game
Supported query parameters:
* `type` - Package types (`mod`, `game`, `txp`).
* `q` - Query string
* `random` - When present, enable random ordering and ignore `sort`.
* `hide` - Hide content based on [Content Flags](content_flags).
* `sort` - Sort by (`name`, `views`, `date`, `score`).
* `order` - Sort ascending (`Asc`) or descending (`desc`).
* `protocol_version` - Only show packages supported by this Minetest protocol version.
* `q`: Query string.
* `type`: Package types (`mod`, `game`, `txp`).
* `sort`: Sort by (`name`, `views`, `created_at`).
* `show_added`: Show topics that have an existing package.
* `limit`: Return at most `limit` topics.
## Collections
* GET `/api/collections/`
* Query args:
* `author`: collection author username.
* `package`: collections that contain the package.
* Returns JSON array of collection entries:
* `author`: author username.
* `name`: collection name.
* `title`
* `short_description`
* `created_at`: creation time in iso format.
* `private`: whether collection is private, boolean.
* `package_count`: number of packages, integer.
* GET `/api/collections/<username>/<name>/`
* Returns JSON object for collection:
* `author`: author username.
* `name`: collection name.
* `title`
* `short_description`
* `long_description`
* `created_at`: creation time in iso format.
* `private`: whether collection is private, boolean.
* `items`: array of item objects:
* `package`: short info about the package.
* `description`: custom short description.
* `created_at`: when the package was added to the collection.
* `order`: integer.
## Types
### Tags
* GET `/api/tags/` ([View](/api/tags/))
* List of objects with:
* `name`: technical name.
* `title`: human-readable title.
* `description`: tag description or null.
* `views`: number of views of this tag.
### Content Warnings
* GET `/api/content_warnings/` ([View](/api/content_warnings/))
* List of objects with
* `name`: technical name
* `title`: human-readable title
* `description`: tag description or null
### Licenses
* GET `/api/licenses/` ([View](/api/licenses/))
* List of objects with:
* `name`
* `is_foss`: whether the license is foss
### Luanti Versions
* GET `/api/minetest_versions/` ([View](/api/minetest_versions/))
* List of objects with:
* `name`: Version name.
* `is_dev`: boolean, is dev version.
* `protocol_version`: protocol version number.
### Languages
* GET `/api/languages/` ([View](/api/languages/))
* List of objects with:
* `id`: language code.
* `title`: native language name.
* `has_contentdb_translation`: whether ContentDB has been translated into this language.
## Misc
* GET `/api/scores/` ([View](/api/scores/))
* See [Top Packages Algorithm](/help/top_packages/).
* Supports [Package Queries](#package-queries).
* Returns list of:
* `author`: package author name.
* `name`: package technical name.
* `downloads`: number of downloads.
* `score`: total package score.
* `score_reviews`: score from reviews.
* `score_downloads`: score from downloads.
* `reviews`: a dictionary of
* `positive`: int, number of positive reviews.
* `neutral`: int, number of neutral reviews.
* `negative`: int, number of negative reviews.
* GET `/api/homepage/` ([View](/api/homepage/)) - get contents of homepage.
* `count`: number of packages
* `downloads`: get number of downloads
* `new`: new packages
* `updated`: recently updated packages
* `pop_mod`: popular mods
* `pop_txp`: popular textures
* `pop_game`: popular games
* `high_reviewed`: highest reviewed
* GET `/api/cdb_schema/` ([View](/api/cdb_schema/))
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
* See [JSON Schema Reference](https://json-schema.org/).
* POST `/api/hypertext/`
* Converts HTML or Markdown to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
to be used in a `hypertext` formspec element.
* Post data: HTML or Markdown as plain text.
* Content-Type: `text/html` or `text/markdown`.
* Query arguments:
* `formspec_version`: Required, maximum supported formspec version. Ie: 6
* `include_images`: Optional, defaults to true. If true, images use `<img>`. If false, they're linked.
* Returns JSON dictionary with following key:
* `head`: markup for suggested styling and custom tags, prepend to the body before displaying.
* `body`: markup for long description.
* `links`: dictionary of anchor name to link URL.
* `images`: dictionary of img name to image URL
* `image_tooltips`: dictionary of img name to tooltip text.

View File

@@ -0,0 +1,74 @@
title: Creating an appealing ContentDB page
## Title and short description
Make sure that your package's title is unique, short, and descriptive.
Expand on the title with the short description. You have a limited number
of characters, use them wisely!
```ini
# Bad, we know this is a mod for Luanti. Doesn't give much information other than "food"
description = The food mod for Luanti
# Much better, says what is actually in this mod!
description = Adds soup, cakes, bakes and juices
```
## Thumbnail
A good thumbnail goes a long way to making a package more appealing. It's one of the few things
a user sees before clicking on your package. Make sure it's possible to tell what a
thumbnail is when it's small.
For a preview of what your package will look like inside Luanti, see
Edit Package > Screenshots.
## Screenshots
Upload a good selection of screenshots that show what is possible with your packages.
You may wish to focus on a different key feature in each of your screenshots.
A lot of users won't bother reading text, and will just look at screenshots.
## Long description
The target audience of your package page is end users.
The long description should explain what your package is about,
why the user should choose it, and how to use it if they download it.
[NodeCore](https://content.luanti.org/packages/Warr1024/nodecore/) is a good
example of what to do. For inspiration, you might want to look at how games on
Steam write their descriptions.
Your long description might contain:
* What does the package contain/have? ie: list of high-level features.
* What makes it special? Why should users choose this over another package?
* How can you use it?
The following are redundant and should probably not be included:
* A heading with the title of the package
* The short description
* Links to a Git repository, the forum topic, the package's ContentDB page (ContentDB has fields for this)
* License (unless you need to give more information than ContentDB's license fields)
* API reference (unless your mod is a library only)
* Development instructions for your package (this should be in the repo's README)
* Screenshots that are already uploaded (unless you want to embed a recipe image in a specific place)
* Note: you should avoid images in the long description as they won't be visible inside Luanti,
when support for showing the long description is added.
## Localize / Translate your package
According to Google Play, 64% of Luanti Android users don't have English as their main language.
Adding translation support to your package increases accessibility. Using content translation, you
can also translate your ContentDB page. See Edit Package > Translation for more information.
<p>
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
{{ _("Translation - Luanti Modding Book") }}
</a>
<a class="btn btn-primary" href="https://api.luanti.org/translations/#translating-content-meta">
{{ _("Translating content meta - lua_api.md") }}
</a>
</p>

View File

@@ -0,0 +1,14 @@
title: Contact Us
## Reports
Please let us know if anything on the ContentDB violates our rules or any applicable
laws.
We take copyright violation and other offenses very seriously.
<a href="/report/" class="btn btn-primary">Report</a>
## Other
<a href="{{ admin_contact_url }}" class="btn btn-primary">Contact the admin</a>

View File

@@ -6,21 +6,36 @@ your client to use new flags.
## Flags
* `nonfree` - can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation.
* A content rating, given below.
Luanti allows you to specify a comma-separated list of flags to hide in the
client:
```
contentdb_flag_blacklist = nonfree, bad_language, drugs
```
## Ratings
A flag can be:
Content ratings aren't currently supported by ContentDB.
Instead, mature content isn't allowed at all for now.
* `nonfree`: can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation.
* `wip`: packages marked as Work in Progress
* `deprecated`: packages marked as Deprecated
* A content warning, given below.
* `*`: hides all content warnings.
In the future, more mature content will be allowed but labelled with
content ratings which may contain the following:
There are also two meta-flags, which are designed so that we can change how different platforms filter the package list
without making a release.
* android_default - meta-rating which includes gore and drugs.
* desktop_default - meta-rating which won't include anything for now.
* gore - more than just blood
* drugs
* swearing
* `android_default`: currently same as `*, deprecated`. Hides all content warnings and deprecated packages
* `desktop_default`: currently same as `deprecated`. Hides deprecated packages
## Content Warnings
Packages with mature content will be tagged with a content warning based
on the content type.
* `alcohol_tobacco`: alcohol or tobacco.
* `bad_language`: swearing.
* `gambling`
* `gore`: blood, etc.
* `horror`: shocking and scary content.
* `violence`: non-cartoon violence.

View File

@@ -0,0 +1,147 @@
title: Copyright Guide
## Why should I care?
Falling foul of copyright law can put you and ContentDB into legal trouble. Receiving a Cease and Desist, DMCA notice,
or a Court Summons isn't pleasant for anyone, and can turn out to be very expensive. This page contains some
guidance on how to ensure your content is clearly licensed and attributed to avoid these issues.
Additionally, ContentDB and the forums both have some
[requirements on the licenses](/policy_and_guidance/#41-allowed-licenses) you are allowed to use. Both require
[free distribution and modification](/help/non_free/), allowing us to remain an open community where people can fork
and remix each other's content. To this end, you need to make sure your content is clearly licensed.
**As always, we are not lawyers and this does not constitute legal advice.**
## What do I need to do?
### Follow the licenses
Make sure you understand the licenses for anything you copy into your content.
[TL;DR Legal](https://tldrlegal.com/license/mit-license) is a good resource for quickly understanding
licenses, although you should actually read the text as well.
If you use code from other sources (such as mods or games), you'll need to make sure you follow
their license. A common one is attribution, you should do this by adding a comment next to the
code and crediting the author in your LICENSE file.
It's sometimes fine to copy trivial/small amounts of code under fair use, but this
is a bit of a grey area. It's better to understand the solution and rewrite it yourself.
### List the sources of your media
It's a good idea to create a list of all the media you used in your package, as it allows
you to keep track of where the media came from. Media includes textures, 3d models,
sounds, and more.
You should have the following information:
* File name (as found in your package)
* Author name
* License
* Source (URL to the webpage, mod name, website name)
It's common to do this in README.md or LICENSE.md like so:
```md
* conquer_arrow_*.png from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC0 1.0.
* conquer_arrow.b3d from [Simple Shooter](https://github.com/stujones11/shooter) by Stuart Jones, CC-BY-SA 3.0.
* conquer_arrow_head.png from MTG, CC-BY-SA 3.0.
* health_*.png from [Gauges](https://content.luanti.org/packages/Calinou/gauges/) by Calinou, CC0.
```
if you have a lot of media, then you can split it up by author like so:
```md
[Kenney](https://www.kenney.nl/assets/voxel-pack), CC0:
* mymod_fence.png
John Green, CC BY-SA 4.0 from [OpenGameArt](https://opengameart.org/content/tiny-16-basic):
* mymod_texture.png
* mymod_another.png
Your Name, CC BY-SA 4.0:
* mymod_texture_i_made.png
```
## Where can I get freely licensed media?
* [OpenGameArt](https://opengameart.org/) - everything
* [Kenney game assets](https://www.kenney.nl/assets) - everything
* [Free Sound](https://freesound.org/) - sounds
* [PolyHaven](https://polyhaven.com/) - 3d models and textures.
* Other Luanti mods/games
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).
If they didn't make it, then go to the actual source to check the license.
## Common Situations
### I made it myself, using X as a guide
Copying by hand is still copying, the law doesn't distinguish this from copy+paste.
Make your own art without copying colors or patterns from existing games/art.
If you need a good set of colors, see [LOSPEC](https://lospec.com/palette-list).
### I got it from Google Images / Search / the Internet
You do not have permission to use things unless you are given permission to do so by the author.
No license is exactly the same as "Copyright &copy; All Rights Reserved".
To use on ContentDB or the forums, you must also be given a clear license.
Try searching with "creative commons" in the search term, and then clicking through to the page
and looking for a license. Make sure the source looks trustworthy, as there are a lot of websites
that rip off art and give an incorrect license. But it might be better to use a trusted source directly, see
[the section above](#where-can-i-get-freely-licensed-media) for a list.
### I have permission from the author
You'll also need to make sure that the author gives you an explicit license for it, such as CC BY-SA 4.0.
Permission for *you* to use it doesn't mean that *everyone* has permission to use it. A license outlines the terms of
the permission, making things clearer and less vague.
### The author said it's free for anyone to use, is that enough?
No, you need an explicit license like CC0 or CC BY-SA 4.0. ContentDB does not allow custom licenses
or public domain.
### I used an AI
Errrr. This is a legally untested area, we highly recommend that **you don't use AI art/code** in packages
for that reason.
For now, we haven't banned AI art/code from ContentDB. Make sure to clearly include it in your package's
credit list (include the name of the AI tool used).
Check the tools terms and conditions to see if there are any constraints on use. It looks
like AI-generated art and code isn't copyrightable by itself, but the tool's T&Cs may still
impose conditions.
AI art/code may regurgitate copyrighted things. Make sure that you don't include the
names of any copyrighted materials in your AI prompts, such as names of games or artists.
## What does ContentDB do?
The package authors and maintainers are responsible for the licenses and copyright of packages on ContentDB.
ContentDB editors will check packages to make sure the package page's license matches up with the list of licenses
inside the package download, but do not investigate each piece of media or line of code.
If a copyright violation is reported to us, we will unlist the package and contact the author/maintainers.
Once the problem has been fixed, the package can be restored. Repeated copyright infringement may lead to
permanent bans.
## Where can I get help?
[Join](https://www.luanti.org/get-involved/) IRC, Matrix, or Discord to ask for help.
In Discord, there are the #assets or #contentdb channels. In IRC or Matrix, you can just ask in the main channels.
If your package is already on ContentDB, you can open a thread.

View File

@@ -0,0 +1,56 @@
title: Editors
## What should editors do?
Editors are users of rank Editor or above.
They are responsible for ensuring that the package listings of ContentDB are useful.
For this purpose, they can/will:
* Review and approve packages.
* Edit any package - including tags, releases, screenshots, and maintainers.
* Create packages on behalf of authors who aren't present.
Editors should make sure they are familiar with the
[Package Inclusion Policy and Guidance](/policy_and_guidance/).
## ContentDB is not a curated platform
It's important to note that ContentDB isn't a curated platform - a mod doesn't need to be good
to be accepted, but there are some minimum requirements when it comes to usefulness and other things.
See 2.2 in the [Policy and Guidance](/policy_and_guidance/).
## Editor Work Queue
The [Editor Work Queue](/todo/) and related pages contain useful information for editors, such as:
* The package, release, and screenshot approval queues.
* Packages which are outdated or are missing tags.
* A list of forum topics without packages.
## Editor Notifications
Editors currently receive notifications for any new thread opened on a package, so that they
know when a user is asking for help. These notifications are shown separately in the notifications
interface, and can be configured separately in Emails and Notifications.
## Crash Course to being an Editor
The [Package Inclusion Policy and Guidance](/policy_and_guidance/) is our go-to resource for making decisions in
changes needed, similar to how lua_api.txt is the doc for modders to consult.
In the [Editor console](/todo/), the two most important tabs are the Editor Work Queue and the Forum
Topics tab. Primarily you will be focusing on the Editor Work Queue tab, where a list of packages to review is.
When you have some free time, feel free to scroll through the Forum Topics page and import mods into ContentDB.
But don't import a mod if it's broken, outdated, not that useful, or not worth importing - click Discard instead.
A simplified process for reviewing a package is as follows:
1. scan the package image if present for any obvious closed source assets.
2. if right to a name warning is present, check its validity and if the package meets
the exceptions.
3. if the forums topic missing warning is present, feel free to check it, but it's
usually incorrect.
4. check source, etc links to make sure they work and are correct.
5. verify that the package has license file that matches what is on the contentdb fields
6. if the above steps pass, approve the package, else request changes needed from the author

68
app/flatpages/help/faq.md Normal file
View File

@@ -0,0 +1,68 @@
title: Frequently Asked Questions
description: FAQ about using ContentDB
## Users and Logins
### How do I create an account?
How you create an account depends on whether you have a forum account.
If you have a forum account, then you'll need to prove that you are the owner of the account. This can
be done using a GitHub account or a random string in your forum account signature.
If you don't, then you can just sign up using an email address and password.
GitHub can only be used to log in, not to register.
<a class="btn btn-primary" href="/user/claim/">Register</a>
### My verification email never arrived
There are a number of reasons this may have happened:
* Incorrect email address entered.
* Temporary problem with ContentDB.
* Email has been unsubscribed.
**When creating an account by email:**
If the email doesn't arrive after registering by email, then you'll need to
try registering again in 12 hours. Unconfirmed accounts are deleted after 12 hours.
**When changing your email (or it was set after a forum-based registration)**:
then you can just set a new email in
[Settings > Email and Notifications](/user/settings/email/).
If you have previously unsubscribed this email, then ContentDB is completely prevented from sending emails to that
address. You'll need to use a different email address, or [contact the admin]({{ admin_contact_url }}) to
remove your email from the blacklist.
## Packages
### How can I create releases automatically?
There are a number of methods:
* [Git Update Detection](/help/update_config/): ContentDB will check your Git repo daily, and create updates or send you notifications.
* [Webhooks](/help/release_webhooks/): you can configure your Git host to send a webhook to ContentDB, and create an update immediately.
* the [API](/help/api/): This is especially powerful when combined with CI/CD and other API endpoints.
### How do I learn how to make mods and games for Luanti?
You should read
[the official Luanti Modding Book](https://rubenwardy.com/minetest_modding_book/)
for a guide to making mods and games using Luanti.
### How do I install something from here?
See [Installing content](/help/installing/).
### How can my package get more downloads?
See [Creating an appealing ContentDB page](/help/appealing_page/).
## How do I get help?
Please [contact rubenwardy](https://rubenwardy.com/contact/).

View File

@@ -0,0 +1,137 @@
title: Featured Packages
<p class="alert alert-warning">
<b>Note:</b> This is a draft, and is likely to change
</p>
## What are Featured Packages?
Featured Packages are shown at the top of the ContentDB homepage. In the future,
featured packages may be shown inside the Luanti client.
The purpose is to promote content that demonstrates a high quality of what is
possible in Luanti. The selection should be varied, and should vary over time.
The featured content should be content that we are comfortable recommending to
a first time player.
## How are the packages chosen?
Before a package can be considered, it must fulfil the criteria in the below lists.
There are three types of criteria:
* "MUST": These must absolutely be fulfilled, no exceptions!
* "SHOULD": Most of them should be fulfilled, if possible. Some of them can be
left out if there's a reason.
* "CAN": Can be fulfilled for bonus points, they are entirely optional.
For a chance to get featured, a package must fulfil all "MUST" criteria and
ideally as many "SHOULD" criteria as possible. The more, the better. Thankfully,
many criteria are trivial to fulfil. Note that ticking off all the boxes is not
enough: Just because a package completes the checklist does not make it good.
Other aspects of the package should be rated as well. See this list as a
starting point, not as an exhaustive quality control.
Editors are responsible for maintaining the list of featured packages. Authors
can request that their package be considered by opening a thread titled
"Feature Package" on their package. To speed things up, they should justify
why they meet (or don't meet) the below criteria. Editors must abstain from
voting on packages where they have a conflict of interest.
A package being featured does not mean that it will be featured forever. A
package may be unfeatured if it no longer meets the criteria, to make space for
other packages to be featured, or for another reason.
## General Requirements
### General
* MUST: Be 100% free and open source (as marked as Free on ContentDB).
* MUST: Work out-of-the-box (no weird setup or settings required).
* MUST: Be compatible with the latest stable Luanti release.
* SHOULD: Use public source control (such as Git).
* SHOULD: Have at least 3 reviews, and be largely positive.
### Stability
* MUST: Be well maintained (author is present and active).
* MUST: Be reasonably stable, with no game-breaking or major bugs.
* MUST: The author does not consider the package to be in an
experimental/development/alpha state. Beta and "unfinished" packages are fine.
* MUST: No error messages from the engine (e.g. missing textures).
* SHOULD: No major map breakages (including unknown nodes, corruption, loss of inventories).
Map breakages are a sign that the package isn't sufficiently stable.
Note: Any map breakage will be excused if "disaster relief" (i.e. tools to repair the damage)
is available.
### Meta and packaging
* MUST: `screenshot.png` is present and up-to-date, with a correct aspect ratio (3:2, at least 300x200).
* MUST: Have a high resolution cover image on ContentDB (at least 1280x720 pixels).
It may be shown cropped to 16:9 aspect ratio, or shorter.
* MUST: mod.conf/game.conf/texture_pack.conf present with:
* name (if mod or game)
* description
* dependencies (if relevant)
* `min_minetest_version` and `max_minetest_version` (if relevant)
* MUST: Contain a README file and a LICENSE file. These may be `.md` or `.txt`.
* README files typically contain helpful links (download, manual, bugtracker, etc), and other
information that players or (potential) contributors may need.
* SHOULD: All important settings are in settingtypes.txt with description.
## Game-specific Requirements
### Meta and packaging
* MUST: Have a main menu icon and header image.
### Stability
* MUST: If any major setting (like `enable_damage`) is unsupported, the game must disable it
using `disabled_settings` in the `game.conf`, and deal with it appropriately in the code
(e.g. force-disable the setting, as the user may still set the setting in `minetest.conf`)
### Usability
* MUST: Unsupported mapgens are disabled in game.conf.
* SHOULD: Passes the Beginner Test: A newbie to the game (but not Luanti) wouldn't get completely
stuck within the first 5 minutes of playing.
* SHOULD: Have good documentation. This may include one or more of:
* A craftguide, or other in-game learning system
* A manual
* A wiki
* Something else
### Gameplay
* CAN: Passes the Six Hour Test (only applies to sandbox games): The game doesn't run out of new
content before the first 6 hours of playing.
* CAN: Players don't feel that something in the game is "lacking".
### Audiovisuals
* MUST: Audiovisual design should be of good quality.
* MUST: No obvious GUI/HUD breakages.
* MUST: Sounds have no obvious artifacts like clicks or unintentional noise.
* SHOULD: Graphical design is mostly consistent.
* SHOULD: Sounds are used.
* SHOULD: Sounds are normalized (more or less).
### Quality Assurance
* MUST: No flooding the console/log file with warnings.
* MUST: No duplicate crafting recipes.
* MUST: Highly experimental game features are disabled by default.
* MUST: Experimental game features are clearly marked as such.
* SHOULD: No unknown nodes/items/objects appear.
* SHOULD: No dependency on legacy API calls.
* SHOULD: No console warnings.
### Writing
* MUST: All items that can be obtained in normal gameplay have `description` set (whether in the definition or meta).
* MUST: Game is not littered with typos or bad grammar (a few typos are OK but should be fixed, when found).
* SHOULD: All items have unique names (items which disguise themselves as another item are exempt).
* SHOULD: The writing style of all item names is grammatical and consistent.
* SHOULD: Descriptions of things convey useful and meaningful information (if applicable).
* CAN: Text is written in clear and (if possible) simple language.

View File

@@ -0,0 +1,16 @@
title: Feeds
You can follow updates from ContentDB in your RSS feed reader. If in doubt, copy the Atom URL.
* All events: [Atom]({{ url_for('feeds.all_atom') }}) | [JSONFeed]({{ url_for('feeds.all_json') }})
* New packages: [Atom]({{ url_for('feeds.packages_all_atom') }}) | [JSONFeed]({{ url_for('feeds.packages_all_json') }})
* New releases: [Atom]({{ url_for('feeds.releases_all_atom') }}) | [JSONFeed]({{ url_for('feeds.releases_all_json') }})
## Package feeds
Follow new releases for a package:
```
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.atom
https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.json
```

View File

@@ -0,0 +1,52 @@
title: Supported Games
## Why?
The supported/compatible games feature allows mods to specify the games that
they work with, which improves user experience.
## Support sources
### mod.conf / texture_pack.conf
You can use `supported_games` to specify games that your mod/modpack/texture
pack is compatible with.
You can use `unsupported_games` to specify games that your package doesn't work
with, which is useful for overriding ContentDB's automatic detection.
Both of these are comma-separated lists of game technical ids. Any `_game`
suffixes are ignored, just like in Luanti.
supported_games = minetest_game, repixture
unsupported_games = lordofthetest, nodecore, whynot
If your package supports all games by default, you can put "*" in
supported_games. You can still use unsupported_games to mark games as
unsupported. You can also specify games that you've tested in supported_games.
# Should work with all games but I've only tested using Minetest Game:
supported_games = *, minetest_game
# But doesn't work in capturetheflag
unsupported_game = capturetheflag
### Dependencies
ContentDB will analyse hard dependencies and work out which games a mod
supports.
This uses a recursive algorithm that works out whether a dependency can be
installed independently, or if it requires a certain game.
### On ContentDB
You can define supported games on ContentDB, but using .conf is recommended
instead.
## Combining all the sources
.conf will override anything ContentDB detects. The manual override on ContentDB
overrides .conf and dependencies.

View File

@@ -0,0 +1,89 @@
title: How to install mods, games, and texture packs
description: A guide to installing mods, games, and texture packs in Luanti.
## Installing from the main menu (recommended)
### Install
1. Open the mainmenu
2. Go to the Content tab and click "Browse online content".
If you don't see this, then you need to update Luanti to v5.
3. Search for the package you want to install, and click "Install".
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".
<div class="row mt-5">
<div class="col-md-6">
<figure>
<a href="/static/installing_content_tab.png">
<img class="w-100" src="/static/installing_content_tab.png" alt="Screenshot of the content tab in Luanti">
</a>
<figcaption class="text-muted ps-1">
1. Click Browser Online Content in the content tab.
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure>
<a href="/static/installing_cdb_dialog.png">
<img class="w-100" src="/static/installing_cdb_dialog.png" alt="Screenshot of the content tab in Luanti">
</a>
<figcaption class="text-muted ps-1">
2. Search for the package and click "Install".
</figcaption>
</figure>
</div>
</div>
Troubleshooting:
* I can't find it in the ContentDB dialog (Browse online content)
* Make sure that you're on the latest version of Luanti.
* Are you using Android? Packages with content warnings are hidden by default on android,
you can show them by removing `android_default` from the `contentdb_flag_blacklist` setting.
* Does the webpage show "Non-free" warnings? Non-free content is hidden by default from all clients,
you can show them by removing `nonfree` from the `contentdb_flag_blacklist` setting.
* It says "required dependencies could not be found"
* Make sure you're using the correct "Base Game". A lot of packages only work with certain games, you can look
at "Compatible Games" on the web page to see which.
### Enable in Select Mods
1. Mods: Enable the content using "Select Mods" when selecting a world.
2. Games: choose a game when making a world.
3. Texture packs: Content > Select pack > Click enable.
<div class="row mt-5">
<div class="col-md-6">
<figure>
<a href="/static/installing_select_mods.png">
<img class="w-100" src="/static/installing_select_mods.png" alt="Screenshot of Select Mods in Luanti">
</a>
<figcaption class="text-muted ps-1">
Enable mods using the Select Mods dialog.
</figcaption>
</figure>
</div>
</div>
## Installing using the command line
### Git clone
1. Install git
2. Find the package on ContentDB and copy "source" link.
3. Find the user data directory.
In 5.4.0 and above, you can click "Open user data directory" in the Credits tab.
Otherwise:
* Windows: wherever you extracted or installed Luanti to.
* Linux: usually `~/.minetest/`
4. Open or create the folder for the type of content (`mods`, `games`, or `textures`)
5. Git clone there
6. For mods, make sure to install any required dependencies.
### Enable
* Mods: Edit world.mt in the world's folder to contain `load_file_MODNAME = true`
* Games: Use `--game` or edit game_id in world.mt.
* Texture packs: change the `texture_path` setting to the texture pack absolute path.

View File

@@ -0,0 +1,25 @@
title: Prometheus Metrics
## What is Prometheus?
[Prometheus](https://prometheus.io) is an "open-source monitoring system with a
dimensional data model, flexible query language, efficient time series database
and modern alerting approach".
Prometheus Metrics can be accessed at [/metrics](/metrics), or you can view them
on the Grafana instance below.
{% if monitoring_url %}
<p>
<a class="btn btn-primary" href="{{ monitoring_url }}">
View ContentDB on Grafana
</a>
</p>
{% endif %}
## Metrics
* `contentdb_packages` - Total packages (counter).
* `contentdb_users` - Number of registered users (counter).
* `contentdb_downloads` - Total downloads (counter).
* `contentdb_score` - Total package score (gauge).

View File

@@ -0,0 +1,83 @@
title: Non-free Licenses
## What are Non-Free, Free, and Open Source licenses?
A non-free license is one that does not meet the
[Free Software Definition](https://www.gnu.org/philosophy/free-sw.en.html)
or the [Open Source Definition](https://opensource.org/osd).
ContentDB will clearly label any packages with non-free licenses,
and they will be subject to limited promotion.
## How does ContentDB deal with Non-Free Licenses?
**ContentDB does not allow certain non-free licenses, and will limit the promotion
of packages with non-free licenses.**
Luanti is free and open source software, and is only as big as it is now
because of this. It's pretty amazing you can take nearly any published mod and modify it
to how you like - add some features, maybe fix some bugs - and then share those
modifications without the worry of legal issues. The project, itself, relies on open
source contributions to survive - if it were non-free, then it would have died
when celeron55 lost interest.
If you have played nearly any game with a large modding scene, you will find
that most mods are legally ambiguous. A lot of them don't even provide the
source code to allow you to bug fix or extend as you need.
Limiting the promotion of problematic licenses helps Luanti avoid ending up in
such a state. Licenses that prohibit redistribution or modification are
completely banned from ContentDB and the Luanti forums. Other non-free licenses
will be subject to limited promotion - they won't be shown by default in
the client.
Not providing full promotion on ContentDB, or not allowing your package at all,
doesn't mean you can't make such content - it just means we're not going to help
you spread it.
## What's so bad about licenses that forbid commercial use?
Please read [reasons not to use a Creative Commons -NC license](https://freedomdefined.org/Licenses/NC).
Here's a quick summary related to Luanti content:
1. They make your work incompatible with a growing body of free content, even if
you do want to allow derivative works or combinations.
This means that it can cause problems when another modder wishes to include your
work in a modpack or game.
2. They may rule out other basic and beneficial uses that you want to allow.
For example, CC -NC will forbid showing your content in a monetised YouTube
video.
3. They are unlikely to increase the potential profit from your work, and a
share-alike license serves the goal to protect your work from unethical
exploitation equally well.
## How can I show non-free packages in the client?
Non-free packages are hidden in the client by default, partly in order to comply
with the rules of various Linux distributions.
Users can opt in to showing non-free software, if they wish:
1. In the main menu, go to Settings > All settings
2. Search for "ContentDB Flag Blacklist".
3. Edit that setting to remove `nonfree, `.
<figure class="figure my-4">
<img class="figure-img img-fluid rounded" src="/static/contentdb_flag_blacklist.png" alt="Screenshot of the ContentDB Flag Blacklist setting">
<figcaption class="figure-caption">Screenshot of the ContentDB Flag Blacklist setting</figcaption>
</figure>
The [`platform_default` flag](/help/content_flags/) is used to control what content
each platforms shows. It doesn't hide anything on Desktop, but hides all mature
content on Android. You may wish to remove all text from that setting completely,
leaving it blank. See [Content Warnings](/help/content_flags/#content-warnings)
for information on mature content.
## How can I hide non-free packages on the website?
Clicking "Hide non-free packages" in the footer of ContentDB will hide non-free packages from search results.
It will not hide non-free packages from user profiles.
## See also
* [List of non-free packages](/packages/?flag=nonfree)
* [Copyright Guide](/help/copyright)

103
app/flatpages/help/oauth.md Normal file
View File

@@ -0,0 +1,103 @@
title: OAuth2 API
<p class="alert alert-warning">
The OAuth2 applications API is currently experimental, and may break without notice.
</p>
ContentDB allows you to create an OAuth2 Application and obtain access tokens
for users.
## Scopes
OAuth2 applications can currently only access public user data, using the whoami API.
## Create an OAuth2 Client
Go to Settings > [OAuth2 Applications](/user/apps/) > Create
## Obtaining access tokens
ContentDB supports the Authorization Code OAuth2 method.
### Authorize
Get the user to open the following URL in a web browser:
```
https://content.luanti.org/oauth/authorize/
?response_type=code
&client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URL}
```
The redirect_url must much the value set in your oauth client. Make sure to URL encode it.
ContentDB also supports `state`.
Afterwards, the user will be redirected to your callback URL.
If the user accepts the authorization, you'll receive an authorization code (`code`).
Otherwise, the redirect_url will not be modified.
For example, with `REDIRECT_URL` set as `https://example.com/callback/`:
* If the user accepts: `https://example.com/callback/?code=abcdef`
* If the user cancels: `https://example.com/callback/`
### Exchange auth code for access token
Next, you'll need to exchange the auth for an access token.
Do this by making a POST request to the `/oauth/token/` API:
```bash
curl -X POST https://content.luanti.org/oauth/token/ \
-F grant_type=authorization_code \
-F client_id="CLIENT_ID" \
-F client_secret="CLIENT_SECRET" \
-F code="abcdef"
```
<p class="alert alert-warning">
<i class="fas fa-exclamation-circle me-2"></i>
You should make this request on a server to prevent the user
from getting access to your client secret.
</p>
If successful, you'll receive:
```json
{
"success": true,
"access_token": "access_token",
"token_type": "Bearer"
}
```
If there's an error, you'll receive a standard API error message:
```json
{
"success": false,
"error": "The error message"
}
```
Possible errors:
* Unsupported grant_type, only authorization_code is supported
* Missing client_id
* Missing client_secret
* Missing code
* client_id and/or client_secret is incorrect
* Incorrect code. It may have already been redeemed
### Check access token
Next, you should check the access token works by getting the user information:
```bash
curl https://content.luanti.org/api/whoami/ \
-H "Authorization: Bearer YOURTOKEN"
```

View File

@@ -0,0 +1,143 @@
title: Package Configuration and Releases Guide
## Introduction
ContentDB will read configuration files in your package when doing several
tasks, including package and release creation. This page details how you can use
this to your advantage.
## .conf files
### What is a content .conf file?
Every type of content can have a `.conf` file that contains the metadata.
The filename of the `.conf` file depends on the content type:
* `mod.conf` for mods.
* `modpack.conf` for mod packs.
* `game.conf` for games.
* `texture_pack.conf` for texture packs.
The `.conf` uses a key-value format, separated using equals.
Here's a simple example of `mod.conf`, `modpack.conf`, or `texture_pack.conf`:
name = mymod
title = My Mod
description = A short description to show in the client.
Here's a simple example of `game.conf`:
title = My Game
description = A short description to show in the client.
Note that you should not specify `name` in game.conf.
### Understood values
ContentDB understands the following information:
* `title` - A human-readable title.
* `description` - A short description to show in the client.
* `depends` - Comma-separated hard dependencies.
* `optional_depends` - Comma-separated soft dependencies.
* `min_minetest_version` - The minimum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
* `max_minetest_version` - The maximum Luanti version this runs on, see [Min and Max Luanti Versions](#min_max_versions).
and for mods only:
* `name` - the mod technical name.
* `supported_games` - List of supported game technical names.
* `unsupported_games` - List of not supported game technical names. Useful to override game support detection.
## .cdb.json
You can include a `.cdb.json` file in the root of your content directory (ie: next to a .conf)
to update the package meta.
It should be a JSON dictionary with one or more of the following optional keys:
* `type`: One of `GAME`, `MOD`, `TXP`.
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
* `short_description`
* `dev_state`: One of `WIP`, `BETA`, `ACTIVELY_DEVELOPED`, `MAINTENANCE_ONLY`, `AS_IS`, `DEPRECATED`,
`LOOKING_FOR_MAINTAINER`.
* `tags`: List of tag names, see [/api/tags/](/api/tags/).
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
* `license`: A license name, see [/api/licenses/](/api/licenses/).
* `media_license`: A license name.
* `long_description`: Long markdown description.
* `repo`: Source repository (eg: Git).
* `website`: Website URL.
* `issue_tracker`: Issue tracker URL.
* `forums`: forum topic ID.
* `video_url`: URL to a video.
* `donate_url`: URL to a donation page.
* `translation_url`: URL to send users interested in translating your package.
Use `null` or `[]` to unset fields where relevant.
Example:
```json
{
"title": "Foo bar",
"tags": ["pvp", "survival"],
"license": "MIT",
"website": null
}
```
## Controlling Release Creation
### Git-based Releases and Submodules
ContentDB can create releases from a Git repository.
It will include submodules in the resulting archive.
Simply set VCS Repository in the package's meta to a Git repository, and then
choose Git as the method when creating a release.
### Automatic Release Creation
See [Git Update Detection](/help/update_config/).
You can also use [GitLab/GitHub webhooks](/help/release_webhooks/) or the [API](/help/api/)
to create releases.
### Min and Max Luanti Versions
<a name="min_max_versions" />
When creating a release, the `.conf` file will be read to determine what Luanti
versions the release supports. If the `.conf` doesn't specify, then it is assumed
that it supports all versions.
This happens when you create a release via the ContentDB web interface, the
[API](/help/api/), or using a [GitLab/GitHub webhook](/help/release_webhooks/).
Here's an example config:
name = mymod
min_minetest_version = 5.0
max_minetest_version = 5.3
Leaving out min or max to have them set as "None".
### Excluding files
When using Git to create releases,
you can exclude files from a release by using [gitattributes](https://git-scm.com/docs/gitattributes):
.* export-ignore
sources export-ignore
*.zip export-ignore
This will prevent any files from being included if they:
* Beginning with `.`
* or are named `sources` or are inside any directory named `sources`.
* or have an extension of "zip".

View File

@@ -1,33 +0,0 @@
title: Package Tags
## Overview
Tags should be added to packages to enable easy identification of different types of mods, games and texture packs.
They are only beneficial when applied correctly, so please use the following guidelines.
## Tag Usage
* **Building** - For mods that focus on adding new materials or nodes to build with.
* **Education** - For mods or games created for educational purposes.
* **Environment** - For mods that add environmental effects, including ambient sound and weather effects.
* **Inventory** - For mods that add new inventory systems or new inventory pages.
* **Machines and Electronics** - For mods that include placeable machinery or electronic components which interact to complete tasks.
* **Maintenance** - For mods that assist with world or player maintenance. This includes large-scale map manipulation, area protection and other administrative tools.
* **Mapgen** - For mods that add new biomes, new mapgen decorations, or any other mapgen elements.
* **Mobs and NPCs** - For mods that add mobs or NPCs, or provide tools that assist with mob and NPC creation or manipulation.
* **Plants and Farming** - For mods that add new plants or other farmable resources.
* **Player effects/Food** - For mods that change player effects, for example speed, jump height or gravity, and food.
* **Tools** - For mods that add new tools or new features for existing tools.
* **Transport** - For mods that add transportation methods. This includes teleportation, vehicles and ridable mobs.
* **Survival** - For mods written specifically for survival games. For example, these mods might focus on game-balance or increase the difficulty level. This tag should also be used for games with a heavy survival focus.
* **Creative** - For mods written specifically (and often exclusively) for use in creative mode. For example, these mods may add a large amount of decorative content, or content without crafting recipes. This tag should also be used for games with a heavy creative focus.
* **Multiplayer-focused** - For games that can be played with other players.
* **Singleplayer-focused** - For games that can be played alone.
* **PvP** - For games designed to be played competitively against other players.
* **PvE** - For games designed for one or multiple players which focus on combat against mobs or NPCs.
* **Puzzle** - For mods and games with a focus on puzzle solving instead of combat.
* **16px** - For 16px texture packs.
* **32px** - For 32px texture packs.
* **64px** - For 64px texture packs.
* **128px+** - For 128px or higher texture packs.

View File

@@ -2,25 +2,27 @@ title: Ranks and Permissions
## Overview
* **New Members** - mostly untrusted, cannot change package meta data or publish releases without approval.
* **Members** - Trusted to change the meta data of their own packages', but cannot publish releases.
* **Trusted Members** - Same as above, but can approve their own releases and packages.
* **Editors** - Trusted to change the meta data of any package, and also make and publish releases.
* **New Members** - mostly untrusted, cannot change package metadata or publish releases without approval.
* **Members** - Trusted to change the metadata of their own packages', but cannot approve their own packages.
* **Trusted Members** - Same as above, but can approve their own releases.
* **Approvers** - Responsible for approving new packages, screenshots, and releases.
* **Editors** - Same as above, and can edit any package or release.
* **Moderators** - Same as above, but can manage users.
* **Admins** - Full access.
## Breakdown
<table class="fancyTable">
<table class="table table-striped ranks-table">
<thead>
<tr>
<th>Rank</th>
<th colspan=2>New Member</th>
<th colspan=2>Member</th>
<th colspan=2>Trusted Member</th>
<th colspan=2>Editor</th>
<th colspan=2>Moderator</th>
<th colspan=2>Admin</th>
<th colspan=2 class="NEW_MEMBER">New Member</th>
<th colspan=2 class="MEMBER">Member</th>
<th colspan=2 class="TRUSTED_MEMBER">Trusted</th>
<th colspan=2 class="APPROVER">Approver</th>
<th colspan=2 class="EDITOR">Editor</th>
<th colspan=2 class="MODERATOR">Moderator</th>
<th colspan=2 class="ADMIN">Admin</th>
</tr>
<tr>
<th>Owner of thing</th>
@@ -36,223 +38,269 @@ title: Ranks and Permissions
<th>N</th>
<th>Y</th>
<th>N</th>
<th>Y</th>
<th>N</th>
</tr>
</thead>
<tbody>
<tr>
<td>Create Package</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Approve Package</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Delete Package</td>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Edit Package</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Edit Maintainers</td>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Add/Delete Screenshot</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Approve Screenshot</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
</tr>
<tr>
<td>Approve EditRequest</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
</tr>
<tr>
<td>Edit EditRequest</td>
<th><sup>1</sup></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Make Release</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Approve Release</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Change Release URL</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>See Private Thread</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Edit Comments</td>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<td></td>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Set Email</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<th><sup>2</sup></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Create Token</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<td></td> <!-- moderator -->
<th><sup>2</sup></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- admin -->
<td></td>
</tr>
<tr>
<td>Set Rank</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th><sup>3</sup></th> <!-- moderator -->
<th><sup>2</sup><sup>3</sup></th>
<th></th> <!-- admin -->
<th></th>
<td></td> <!-- new -->
<td></td>
<td></td> <!-- member -->
<td></td>
<td></td> <!-- trusted member -->
<td></td>
<td></td> <!-- approver -->
<td></td>
<td></td> <!-- editor -->
<td></td>
<th><sup>2</sup></th> <!-- moderator -->
<th><sup>1</sup><sup>2</sup></th>
<td></td> <!-- admin -->
<td></td>
</tr>
</tbody>
</table>
1. User must be the author of the EditRequest.
2. Target user cannot be an admin.
3. Cannot set user to a higher rank than themselves.
1. Target user cannot be an admin.
2 Cannot set user to a higher rank than themselves.

View File

@@ -6,76 +6,66 @@ A webhook is a notification from one service to another. Put simply, a webhook
is used to notify ContentDB that the git repository has changed.
ContentDB offers the ability to automatically create releases using webhooks
from either Github or Gitlab. If you're not using either of those services,
from either GitHub or GitLab. If you're not using either of those services,
you can also use the [API](../api) to create releases.
ContentDB also offers the ability to poll a Git repo and check for updates
without any web hooks, this is limited to once a day.
See [Git Update Detection](/help/update_config/).
The process is as follows:
1. The user creates an API Token and a webhook to use it. This can be done automatically
for Github.
2. The user pushes a commit to the git host (Gitlab or Github).
1. The user creates an API Token and a webhook to use it.
2. The user pushes a commit to the git host (GitLab or GitHub).
3. The git host posts a webhook notification to ContentDB, using the API token assigned to it.
4. ContentDB checks the API token and issues a new release.
* If multiple packages match, then only the first will have a release created.
### Branch filtering
By default, "New commit" or "push" based webhooks will only work on "master"/"main" branches.
You can configure the branch used by changing "Branch name" in [Git update detection](update_config).
For example, to support production and beta packages you can have multiple packages with the same VCS repo URL
but different [Git update detection](update_config) branch names.
Tag-based webhooks are accepted on any branch.
<p class="alert alert-info">
This feature is in beta, and is only available for Trusted Members.
</p>
## Setting up
### GitHub (automatic)
### GitHub
1. Go to your package's page.
2. Make sure that the repository URL is set to a Github repository.
Only github.com is supported.
3. Go to "Releases" > "+", and click "Setup webhook" at the top of the create release
page.
If you do not see this, either the repository isn't using Github or you do
not have permission to use webhook releases (ie: you're not a Trusted Member).
4. Grant ContentDB the ability to manage Webhooks.
5. Set the event to either "New tag or Github Release" (highly recommended) or "Push".
N.B.: GitHub uses tags to power GitHub Releases, meaning that creating a webhook
on "New tag" will sync GitHub and ContentDB releases.
### GitHub (manual)
1. Create an API Token by visiting your profile and clicking "API Tokens: Manage".
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated.
3. Go to the repository's settings > Webhooks > Add Webhook.
4. Set the payload URL to `https://content.minetest.net/github/webhook/`
3. Go to the GitLab repository's settings > Webhooks > Add Webhook.
4. Set the payload URL to `https://content.luanti.org/github/webhook/`
5. Set the content type to JSON.
6. Set the secret to the access token that you copied.
7. Set the events
* If you want a rolling release, choose "just the push event".
* Or if you want a stable release cycle based on tags,
choose "Let me select" > Branch or tag creation.
* If you want a rolling release, choose "just the push event".
* Or if you want a stable release cycle based on tags, choose "Let me select" > Branch or tag creation.
8. Create.
9. If desired, change [Git update detection](update_config) > Branch name to configure the [branch filtering](#branch-filtering).
### GitLab (manual)
### GitLab
1. Create an API Token by visiting your profile and clicking "API Tokens: Manage".
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
2. Copy the access token that was generated.
3. Go to the repository's settings > Integrations.
4. Set the URL to `https://content.minetest.net/gitlab/webhook/`
6. Set the secret token to the access token that you copied.
3. Go to the GitLab repository's settings > Webhooks.
4. Set the URL to `https://content.luanti.org/gitlab/webhook/`
6. Set the secret token to the ContentDB access token that you copied.
7. Set the events
* If you want a rolling release, choose "Push events".
* Or if you want a stable release cycle based on tags,
choose "Tag push events".
8. Add webhook.
9. If desired, change [Git update detection](update_config) > Branch name to configure the [branch filtering](#branch-filtering).
## Configuring
## Configuring Release Creation
### Setting minimum and maximum Minetest versions
See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation.
<p class="alert alert-info">
This feature is unimplemented.
</p>
1. Open up the conf file for the package.
This will be `game.conf`, `mod.conf`, `modpack.conf`, or `texture_pack.conf`
depending on the content type.
2. Set `min_protocol` and `max_protocol` to the respective protocol numbers
of the Minetest versions.
* 0.4 = 32
* 5.0 = 37
* 5.1 = 38
From the Git repository, you can set the min/max Luanti versions, which files are included,
and update the package meta.

View File

@@ -1,8 +0,0 @@
title: Reporting Content
Please let us know if anything on the ContentDB violates our rules or any applicable
laws.
We take copyright violation and other offenses very seriously.
<a href="https://rubenwardy.com/contact/" class="btn btn-success">Contact</a>

View File

@@ -0,0 +1,36 @@
title: Top Packages Algorithm
## Package Score
Each package is given a `score`, which is used when ordering them in the
"Top Games/Mods/Texture Packs" lists. The intention of this feature is
to make it easier for new users to find good packages.
A package's score is equal to a rolling average of recent downloads,
plus the sum of the score given by reviews.
A review score is 100 if positive, -100 if negative.
```c
reviews_sum = sum(100 * (positive ? 1 : -1));
score = avg_downloads + reviews_sum;
```
## Pseudo rolling average of downloads
Each package adds 1 to `avg_downloads` for each unique download,
and then loses 6.66% (=1/15) of the value every day.
This is called a [Frecency](https://en.wikipedia.org/wiki/Frecency) heuristic,
a measure which combines both frequency and recency.
"Unique download" is counted per IP per package.
Downloading an update won't increase the download count if it's already been
downloaded from that IP.
## Transparency and Feedback
You can see all scores using the [scores REST API](/api/scores/), or by
using the [Prometheus metrics](/help/metrics/) endpoint.
Consider [suggesting improvements](https://github.com/luanti-org/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).

View File

@@ -0,0 +1,43 @@
title: Git Update Detection
## Introduction
When you push a change to your Git repository, ContentDB can create a new release automatically or
send you a reminder. ContentDB will check your Git repository one per day, but you can use
webhooks or the API for faster updates.
Git Update Detection is clever enough to not create a release again if you've already created
it manually or using webhooks/the API.
## Setting up
* Set "VCS Repository URL" in your package.
* Open the "Configure Git Update Detection" page:
* Go to the Create Release page and click "Set up" on the banner.
* If the "How do you want to create releases?" wizard appears, choose "Automatic".
* Choose a trigger:
* **New Commit** - this will trigger for each pushed commit on the default branch, or the branch you specify.
* **New Tag** - this will trigger when a New Tag is created.
* Choose action to occur when the trigger happens:
* **Notification** - All maintainers receive a notification under the Bot category, and the package
will appear under "Outdated Packages" in [your to do list](/user/todo/).
* **Create Release** - A new release is created.
If New Commit, the title will be the iso date (eg: 2021-02-01).
If New Tag, the title will the tag name.
## Marking a package as up-to-date
Git Update Detection shouldn't erroneously mark packages as outdated if it is configured currently,
so the first thing you should do is make sure the Update Settings are set correctly.
There are some situations where the settings are correct, but you want to mark a package as
up-to-date - for example, if you don't want to make a release for a particular tag.
Clicking "Save" on "Update Settings" will mark a package as up-to-date.
## Configuring Release Creation
See the [Package Configuration and Releases Guide](/help/package_config/) for
documentation on configuring the release creation.
From the Git repository, you can set the min/max Luanti versions, which files are included,
and update the package meta.

View File

@@ -1,26 +1,5 @@
title: WTFPL is a terrible license
no_h1: true
<div id="warning" class="alert alert-warning">
<span class="icon_message"></span>
Please reconsider the choice of WTFPL as a license.
<script src="/static/jquery.min.js"></script>
<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
$("#warning").hide();
</script>
</div>
# WTFPL is a terrible license
toc: False
The use of WTFPL as a license is discouraged for multiple reasons.
@@ -39,4 +18,4 @@ license, saying:<sup>[3]</sup>
1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html)
2. [FSF](https://www.gnu.org/licenses/license-list.en.html)
3. [OSI](https://opensource.org/minutes20090304)
3. [OSI](https://opensource.org/meeting-minutes/minutes20090304)

View File

@@ -1,69 +1,66 @@
title: Package Inclusion Policy and Guidance
<div class="alert alert-warning">
<b>Note:</b> This is a draft
</div>
## 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 package listings, except for
donation and personal website links which are permitted in the
long description. <sup>5</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
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.
Also see the [help page on tags](/help/package_tags/).
## 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.
Mature content, including that relating to drugs, excessive gore, violence, or
excessive horror, is not currently permitted - but will be in the future.
Other mature content is permitted providing that it is labelled with the applicable
[content warning](/help/content_flags/).
The submission of malware is strictly prohibited. This includes software which
does not do as it advertises, for example if it posts telemetry without stating
clearly that it does in the package meta.
### 2.2. Useful Content / State of Completion
### 2.2. State of Completion
ContentDB is for playable and useful content - content which is sufficiently
complete to be useful to end-users.
ContentDB should only currently contain playable content - content which is
sufficiently complete to be useful to end users. It's fine to add stuff which
is still a work in progress (WIP) as long as it adds sufficient value -
MineClone 2 is a good example of a WIP package which may break between releases
but still has value. Note that this doesn't mean that you should add a thing
you started working on yesterday, it's worth adding all the basic stuff to
make your package useful.
It's fine to add stuff which is still a Work in Progress (WIP) as long as it
adds sufficient value. You must make sure to mark Work in Progress stuff as
such in the "maintenance status" dropdown, as this will help advise players.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine
and encouraged. ContentDB isn't just for player-facing things, and adding
libraries allows them to be installed when a mod depends on it.
Adding non-player facing mods, such as libraries and server tools, is perfectly
fine and encouraged. ContentDB isn't just for player-facing things and adding
libraries allows Luanti to automatically install dependencies.
### 2.3. Language
We require packages to be in English with (optional) client-side translations for
other languages. This is because Luanti currently requires English as the base language
([Issue to change this](https://github.com/luanti-org/luanti/issues/6503)).
Your package's title and short description must be in English. You can use client-side
translations to [translate content meta](https://api.luanti.org/translations/#translating-content-meta).
### 2.4. Attempt to contribute before forking
You should attempt to contribute upstream before forking a package. If you choose
to fork, you should have a justification (different objectives, maintainer is unavailable, etc).
You should use a different title and make it clear in the long description what the
benefit of your fork is over the original package.
### 2.5. Copyright and trademarks
Your package must not violate copyright or trademarks. You should avoid the use of
trademarks in the package's title or short description. If you do use a trademark,
ensure that you phrase it in a way that does not imply official association or
endorsement.
## 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.
The first package to use a name based on the creation of its forum topic or
contentdb submission has the right to the technical name. The use of a package
ContentDB submission has the right to the technical name. The use of a package
on a server or in private doesn't reserve its name. No other packages of the same
type may use the same name, except for the exception given by 3.2.
@@ -76,47 +73,75 @@ to change the name of the package, or your package won't be accepted.
We reserve the right to issue exceptions for this where we feel necessary.
### 3.2 Mod Forks and Reimplementations
### 3.2. Forks and Reimplementations
An exception to the above is that mods are allowed to have the same name as a
mod if its 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.
mod if it's a fork of that mod (or a close reimplementation). In real terms, it
must be possible to use the new mod as a drop-in replacement.
We reserve the right to decide whether a mod counts as a fork or
reimplementation of the mod that owns the name.
### 3.3. Game Mod Namespacing
New mods introduced by a game must have a unique common prefix to avoid conflicts with
other games and standalone mods. For example, the NodeCore game's first-party mods all
start with `nc_`: `nc_api`, `nc_doors`.
You may include existing or standard mods in your game without renaming them to use the
namespace. For example, NodeCore could include the `awards` mod without needing to rename it.
Standalone mods may not use a game's namespace unless they have been given permission by
the game's author.
The exception given by 3.2 also applies to game namespaces - you may use another game's
prefix if your game is a fork.
## 4. Licenses
### 4.1 Allowed Licenses
### 4.1. License file
You must have a LICENSE, LICENSE.txt, or LICENSE.md file describing the licensing of your package.
Please ensure that you correctly credit any resources (code, assets, or otherwise)
that you have used in your package.
**The use of licenses which do not allow derivatives or redistribution is not
You may use lowercase or include a suffix in the filename (ie: `license-code.txt`). If
you are making a game or modpack, your top level license file may just be a summary or
refer to the license files of individual components.
For help on doing copyright correctly, see the [Copyright help page](/help/copyright/).
### 4.2. Allowed Licenses
**The use of licenses that do not allow derivatives or redistribution is not
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
The use of licenses which discriminate between groups of people or forbid the use
The use of licenses that discriminate between groups of people or forbid the use
of the content on servers or singleplayer is also not permitted.**
However, closed sourced licenses are allowed if they allow the above.
If the license you use is not on the list then please select "Other", and we'll
get around to adding it.
get around to adding it. We reject custom/untested licenses and reserve the right
to decide whether a license should be included.
Please note that the definitions of "free" and "non-free" is the same as that
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
### 4.2 Recommended Licenses
### 4.3. Recommended Licenses
It is highly recommended that you use a free and open source software license.
FOSS licenses result in a sharing community and will increase the number of potential users your package has.
Using a closed source license will result in your package being massively penalised in the search results and package lists.
It is highly recommended that you use a Free and Open Source software (FOSS)
license. FOSS licenses result in a sharing community and will increase the
number of potential users your package has. Using a closed source license will
result in your package not being shown in Luanti by default. See the help page
on [non-free licenses](/help/non_free/) for more information.
It is recommended that you use a proper license for code with a warranty
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
for media, such as a Creative Commons license.
The use of WTFPL is discouraged as it doesn't contain a [valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
The use of WTFPL is discouraged as it doesn't contain a
[valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
and also includes swearing which prevents settings like schools from using your content.
[Read more](/help/wtfpl/).
@@ -125,8 +150,8 @@ Public domain is not a valid license in many countries, please use CC0 or MIT in
## 5. Promotions and Advertisements (inc. asking for donations)
You may note place any promotions or advertisements in any meta data including
screensthos. This includes asking for donations, promoting online shops,
You may not place any promotions or advertisements in any metadata including
screenshots. This includes asking for donations, promoting online shops,
or linking to personal websites and social media. Please instead use the
fields provided on your user profile page to place links to websites and
donation pages.
@@ -135,6 +160,61 @@ ContentDB is for the community. We may remove any promotions if we feel that
they're inappropriate.
## 6. Reporting Violations
## 6. Reviews and Package Score
See the [Reporting Content](/help/reporting/) page.
You may invite players to review your package(s). One way to do this is by sharing the link found in the
"Share and Badges" page of the package's settings.
You must not require anyone to review a package. You must not promise or provide incentives for reviewing a package,
including but not limited to monetary rewards, in-game items, features, and/or privileges.
You may give a cosmetic-only role or badge to those who review your package - this must not be tied to the content or
rating of the review.
You must not attempt to unfairly manipulate your package's ranking, whether by reviews or any other method.
Doing so may result in temporary or permanent suspension from ContentDB.
## 7. Screenshots
1. We require all packages to have at least one screenshot. For packages without visual
content, we recommend making a symbolic image with icons, graphics, or text to depict
the package.
2. **Screenshots must not violate copyright.** This means don't just copy images
from Google search, see [the copyright guide](/help/copyright/).
3. **Screenshots must depict the actual content of the package in some way, and
not be misleading.**
Do not use idealized mockups or blender concept renders if they do not
accurately reflect in-game appearance.
Content in screenshots that is prominently displayed or "focal" should be
either present in, or interact with, the package in some way. These can
include things in other packages if they have a dependency relationship
(either way), or if the submitted package in some way enhances, extends, or
alters that content.
Unrelated package content can be allowed to show what the package content
will look like in a typical/realistic game scene, but should be "in the
background" only as far as possible.
4. **Screenshots must only contain content appropriate for the Content Warnings of
the package.**
## 8. Security
The submission of malware is strictly prohibited. This includes software that
does not do as it advertises, for example, if it posts telemetry without stating
clearly that it does in the package meta.
Packages must not ask that users disable mod security (`secure.enable_security`).
Instead, they should use the insecure environment API.
Packages must not contain obfuscated code.
## 9. Reporting Violations
Please click "Report" on the package page.

View File

@@ -0,0 +1,118 @@
title: Privacy Policy
---
Last Updated: 2024-04-30
([View updates](https://github.com/luanti-org/contentdb/commits/master/app/flatpages/privacy_policy.md))
## What Information is Collected
**All users:**
* HTTP requests are logged, with the following information:
* Time
* IP address
* Page URL
* Platform and Operating System
* Preferred language/locale. This defaults to the browser's locale, but can be changed by the user
* Whether an IP address has downloaded a particular package in the last 14 days
**With an account:**
* Email address
* Passwords (hashed and salted using BCrypt)
* Profile information, such as website URLs and donation URLs
* Comments, threads, and reviews
* Audit log actions (such as edits and logins) and their time stamps
ContentDB collects usernames of content creators from the forums,
as this is required to index forum topics.
Packages, including releases, screenshots, and any meta information,
are not considered personal information.
Please avoid giving other personal information as we do not want it.
## How this information is used
* Logged HTTP requests may be used for debugging ContentDB and combating abuse.
* Email addresses are used to:
* Provide essential system messages, such as password resets and privacy policy updates.
* Send notifications - the user may configure this to their needs, including opting out.
* The admin may use ContentDB to send emails when they need to contact a user.
* Passwords are used to authenticate the user.
* The audit log is used to record actions that may be harmful.
* Preferred language/locale is used to translate emails and the ContentDB interface.
* Requests (such as downloads) are used for aggregated statistics and for
calculating the popularity of packages. For example, download counts are shown
for each package and release and there are also download graphs available for
each package.
* Whether an IP address has downloaded a package or release is cached to prevent
downloads from being counted multiple times per IP address, but this
information is deleted after 14 days.
* IP addresses are used to monitor and combat abuse.
* Other information is displayed as part of ContentDB's service.
## Who has access
* Only the admin has access to the HTTP requests.
The logs may be shared with others to aid in debugging, but care will be taken to remove any personal information.
* Encrypted backups may be shared with selected Luanti staff members (moderators + core devs).
The keys and the backups themselves are given to different people,
requiring at least two staff members to read a backup.
* Email addresses are visible to moderators and the admin.
They have access to assist users, and they are not permitted to share email addresses.
* Hashing protects passwords from being read whilst stored in the database or in backups.
* Profile information is public, including URLs and linked accounts.
* The visibility of comments depends on the visibility of threads.
They are either public, or visible only to the package author and editors.
* The complete audit log is visible to moderators.
Users may see their own audit log actions on their account settings page.
Owners, maintainers, and editors can see the actions on a package.
* Preferred language can only be viewed by those with access to the database or a backup.
* We may be required to share information with law enforcement.
## Third-parties
We do not share any personal information with third parties.
We use <a href="https://sentry.io/">Sentry.io</a> for error logging and performance monitoring.
## Location
The ContentDB production server is currently located in Germany.
Backups are stored in the UK.
Encrypted backups may be stored in other countries, such as the US or EU.
By using this service, you give permission for the data to be moved within the
United Kingdom and/or EU.
## Period of Retention
Logged HTTP requests are automatically deleted within 2 weeks.
Usernames may be kept indefinitely, but other user information will be deleted
if requested. See below.
Whether an IP address has downloaded a package or release is deleted after 14 days.
## Removal Requests
Please [raise a report](/report/?anon=0) if you wish to remove your personal
information.
ContentDB keeps a record of each username and forum topic on the forums, for use
in indexing mod/game topics. ContentDB also requires the use of a username to
uniquely identify a package. Therefore, an author cannot be removed completely
from ContentDB if they have any packages or mod/game topics on the forum.
If we are unable to remove your account for one of the above reasons, your user
account will instead be wiped and deactivated, ending up exactly like an author
who has not yet joined ContentDB. All personal information will be removed from
the profile, and any comments or threads will be deleted.
## Future Changes to Privacy Policy
We will alert any future changes to the privacy policy via notices on the
ContentDB website.
By continuing to use this service, you agree to the privacy policy.

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.

24
app/logic/LogicError.py Normal file
View File

@@ -0,0 +1,24 @@
# ContentDB
# Copyright (C) 2021 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/>.
class LogicError(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
def __str__(self):
return repr("LogicError {}: {}".format(self.code, self.message))

0
app/logic/__init__.py Normal file
View File

148
app/logic/approval_stats.py Normal file
View File

@@ -0,0 +1,148 @@
# ContentDB
# Copyright (C) 2024 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 datetime
from collections import namedtuple, defaultdict
from typing import Dict, Optional
from sqlalchemy import or_
from app.models import AuditLogEntry, db, PackageState
class PackageInfo:
state: Optional[PackageState]
first_submitted: Optional[datetime.datetime]
last_change: Optional[datetime.datetime]
approved_at: Optional[datetime.datetime]
wait_time: int
total_approval_time: int
is_in_range: bool
events: list[tuple[str, str, str]]
def __init__(self):
self.state = None
self.first_submitted = None
self.last_change = None
self.approved_at = None
self.wait_time = 0
self.total_approval_time = -1
self.is_in_range = False
self.events = []
def __lt__(self, other):
return self.wait_time < other.wait_time
def __dict__(self):
return {
"first_submitted": self.first_submitted.isoformat(),
"last_change": self.last_change.isoformat(),
"approved_at": self.approved_at.isoformat() if self.approved_at else None,
"wait_time": self.wait_time,
"total_approval_time": self.total_approval_time if self.total_approval_time >= 0 else None,
"events": [ { "date": x[0], "by": x[1], "title": x[2] } for x in self.events ],
}
def add_event(self, created_at: datetime.datetime, causer: str, title: str):
self.events.append((created_at.isoformat(), causer, title))
def get_state(title: str):
if title.startswith("Approved "):
return PackageState.APPROVED
assert title.startswith("Marked ")
for state in PackageState:
if state.value in title:
return state
if "Work in Progress" in title:
return PackageState.WIP
raise Exception(f"Unable to get state for title {title}")
Result = namedtuple("Result", "editor_approvals packages_info avg_turnaround_time max_turnaround_time")
def _get_approval_statistics(entries: list[AuditLogEntry], start_date: Optional[datetime.datetime] = None, end_date: Optional[datetime.datetime] = None) -> Result:
editor_approvals = defaultdict(int)
package_info: Dict[str, PackageInfo] = {}
ignored_packages = set()
turnaround_times: list[int] = []
for entry in entries:
package_id = str(entry.package.get_id())
if package_id in ignored_packages:
continue
info = package_info.get(package_id, PackageInfo())
package_info[package_id] = info
is_in_range = (((start_date is None or entry.created_at >= start_date) and
(end_date is None or entry.created_at <= end_date)))
info.is_in_range = info.is_in_range or is_in_range
new_state = get_state(entry.title.replace("", "") + (entry.description or ""))
if new_state == info.state:
continue
info.add_event(entry.created_at, entry.causer.username if entry.causer else None, new_state.value)
if info.state == PackageState.READY_FOR_REVIEW:
seconds = int((entry.created_at - info.last_change).total_seconds())
info.wait_time += seconds
if is_in_range:
turnaround_times.append(seconds)
if new_state == PackageState.APPROVED:
ignored_packages.add(package_id)
info.approved_at = entry.created_at
if is_in_range:
editor_approvals[entry.causer.username] += 1
if info.first_submitted is not None:
info.total_approval_time = int((entry.created_at - info.first_submitted).total_seconds())
elif new_state == PackageState.READY_FOR_REVIEW:
if info.first_submitted is None:
info.first_submitted = entry.created_at
info.state = new_state
info.last_change = entry.created_at
packages_info_2 = {}
package_count = 0
for package_id, info in package_info.items():
if info.first_submitted and info.is_in_range:
package_count += 1
packages_info_2[package_id] = info
if len(turnaround_times) > 0:
avg_turnaround_time = sum(turnaround_times) / len(turnaround_times)
max_turnaround_time = max(turnaround_times)
else:
avg_turnaround_time = 0
max_turnaround_time = 0
return Result(editor_approvals, packages_info_2, avg_turnaround_time, max_turnaround_time)
def get_approval_statistics(start_date: Optional[datetime.datetime] = None, end_date: Optional[datetime.datetime] = None) -> Result:
entries = AuditLogEntry.query.filter(AuditLogEntry.package).filter(or_(
AuditLogEntry.title.like("Approved %"),
AuditLogEntry.title.like("Marked %"))
).order_by(db.asc(AuditLogEntry.created_at)).all()
return _get_approval_statistics(entries, start_date, end_date)

361
app/logic/game_support.py Normal file
View File

@@ -0,0 +1,361 @@
# 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 List, Dict, Optional, Tuple
import sqlalchemy
from app.models import PackageType, Package, PackageState, PackageGameSupport
from app.utils import post_bot_message
minetest_game_mods = {
"beds", "boats", "bucket", "carts", "default", "dungeon_loot", "env_sounds", "fire", "flowers",
"give_initial_stuff", "map", "player_api", "sethome", "spawn", "tnt", "walls", "wool",
"binoculars", "bones", "butterflies", "creative", "doors", "dye", "farming", "fireflies", "game_commands",
"keys", "mtg_craftguide", "screwdriver", "sfinv", "stairs", "vessels", "weather", "xpanes",
}
mtg_mod_blacklist = {
"pacman", "tutorial", "runorfall", "realtest_mt5", "mevo", "xaenvironment",
"survivethedays", "holidayhorrors",
}
class GSPackage:
author: str
name: str
type: PackageType
provides: set[str]
depends: set[str]
user_supported_games: set[str]
user_unsupported_games: set[str]
detected_supported_games: set[str]
supports_all_games: bool
detection_disabled: bool
is_confirmed: bool
errors: set[str]
def __init__(self, author: str, name: str, type: PackageType, provides: set[str]):
self.author = author
self.name = name
self.type = type
self.provides = provides
self.depends = set()
self.user_supported_games = set()
self.user_unsupported_games = set()
self.detected_supported_games = set()
self.supports_all_games = False
self.detection_disabled = False
self.is_confirmed = type == PackageType.GAME
self.errors = set()
# For dodgy games, discard MTG mods
if self.type == PackageType.GAME and self.name in mtg_mod_blacklist:
self.provides.difference_update(minetest_game_mods)
@property
def id_(self) -> str:
return f"{self.author}/{self.name}"
@property
def supported_games(self) -> set[str]:
ret = set()
ret.update(self.user_supported_games)
if not self.detection_disabled:
ret.update(self.detected_supported_games)
ret.difference_update(self.user_unsupported_games)
return ret
@property
def unsupported_games(self) -> set[str]:
return self.user_unsupported_games
def add_error(self, error: str):
return self.errors.add(error)
class GameSupport:
packages: Dict[str, GSPackage]
modified_packages: set[GSPackage]
def __init__(self):
self.packages = {}
self.modified_packages = set()
@property
def all_confirmed(self):
return all([x.is_confirmed for x in self.packages.values()])
@property
def has_errors(self):
return any([len(x.errors) > 0 for x in self.packages.values()])
@property
def error_count(self):
return sum([len(x.errors) for x in self.packages.values()])
@property
def all_errors(self) -> set[str]:
errors = set()
for package in self.packages.values():
for err in package.errors:
errors.add(package.id_ + ": " + err)
return errors
def add(self, package: GSPackage) -> GSPackage:
self.packages[package.id_] = package
return package
def get(self, id_: str) -> Optional[GSPackage]:
return self.packages.get(id_)
def get_all_that_provide(self, modname: str) -> List[GSPackage]:
return [package for package in self.packages.values() if modname in package.provides]
def get_all_that_depend_on(self, modname: str) -> List[GSPackage]:
return [package for package in self.packages.values() if modname in package.depends]
def _get_supported_games_for_modname(self, depend: str, visited: list[str]):
dep_supports_all = False
for_dep = set()
for provider in self.get_all_that_provide(depend):
found_in = self._get_supported_games(provider, visited)
if found_in is None:
# Unsupported, keep going
pass
elif len(found_in) == 0:
dep_supports_all = True
break
else:
for_dep.update(found_in)
return dep_supports_all, for_dep
def _get_supported_games_for_deps(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
ret = set()
for depend in package.depends:
dep_supports_all, for_dep = self._get_supported_games_for_modname(depend, visited)
if dep_supports_all:
# Dep is game independent
pass
elif len(for_dep) == 0:
package.add_error(f"Unable to fulfill dependency {depend}")
return None
elif len(ret) == 0:
ret = for_dep
else:
ret.intersection_update(for_dep)
if len(ret) == 0:
package.add_error("Game support conflict, unable to install package on any games")
return None
return ret
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
if package.id_ in visited:
return None
if package.type == PackageType.GAME:
return {package.name}
elif package.is_confirmed:
return package.supported_games
visited = visited.copy()
visited.append(package.id_)
ret = self._get_supported_games_for_deps(package, visited)
if ret is None:
assert len(package.errors) > 0
return None
ret = ret.copy()
ret.difference_update(package.user_unsupported_games)
package.detected_supported_games = ret
self.modified_packages.add(package)
if len(ret) > 0:
for supported in package.user_supported_games:
if supported not in ret:
package.add_error(f"`{supported}` is specified in supported_games but it is impossible to run {package.name} in that game. " +
f"Its dependencies can only be fulfilled in {', '.join([f'`{x}`' for x in ret])}. " +
"Check your hard dependencies.")
if package.supports_all_games:
package.add_error(
"This package cannot support all games as some dependencies require specific game(s): " +
", ".join([f'`{x}`' for x in ret]))
package.is_confirmed = True
return package.supported_games
def on_update(self, package: GSPackage, old_provides: Optional[set[str]] = None):
to_update = {package}
checked = set()
while len(to_update) > 0:
current_package = to_update.pop()
if current_package.id_ in self.packages and current_package.type != PackageType.GAME:
self._get_supported_games(current_package, [])
provides = current_package.provides
if current_package == package and old_provides is not None:
provides = provides.union(old_provides)
for modname in provides:
for depending_package in self.get_all_that_depend_on(modname):
if depending_package not in checked:
if depending_package.id_ in self.packages and depending_package.type != PackageType.GAME:
depending_package.is_confirmed = False
depending_package.detected_supported_games = []
to_update.add(depending_package)
checked.add(depending_package)
def on_remove(self, package: GSPackage):
del self.packages[package.id_]
self.on_update(package)
def on_first_run(self):
for package in self.packages.values():
if not package.is_confirmed:
self.on_update(package)
def _convert_package(support: GameSupport, package: Package) -> GSPackage:
# Unapproved packages shouldn't be considered to fulfill anything
provides = set()
if package.state == PackageState.APPROVED:
provides = set([x.name for x in package.provides])
gs_package = GSPackage(package.author.username, package.name, package.type, provides)
gs_package.depends = set([x.meta_package.name for x in package.dependencies if not x.optional])
gs_package.detection_disabled = not package.enable_game_support_detection
gs_package.supports_all_games = package.supports_all_games
existing_game_support = (package.supported_games
.filter(PackageGameSupport.game.has(state=PackageState.APPROVED),
PackageGameSupport.confidence > 5)
.all())
if not package.supports_all_games:
gs_package.user_supported_games = [x.game.name for x in existing_game_support if x.supports]
gs_package.user_unsupported_games = [x.game.name for x in existing_game_support if not x.supports]
return support.add(gs_package)
def _create_instance(session: sqlalchemy.orm.Session) -> GameSupport:
support = GameSupport()
packages: List[Package] = (session.query(Package)
.filter(Package.state == PackageState.APPROVED, Package.type.in_([PackageType.GAME, PackageType.MOD]))
.all())
for package in packages:
_convert_package(support, package)
return support
def _persist(session: sqlalchemy.orm.Session, support: GameSupport):
for gs_package in support.packages.values():
if len(gs_package.errors) != 0:
msg = "\n".join([f"- {x}" for x in gs_package.errors])
package = session.query(Package).filter(
Package.author.has(username=gs_package.author),
Package.name == gs_package.name).one()
post_bot_message(package, "Error when checking game support", msg, session)
for gs_package in support.modified_packages:
if not gs_package.detection_disabled:
package = session.query(Package).filter(
Package.author.has(username=gs_package.author),
Package.name == gs_package.name).one()
# Clear existing
session.query(PackageGameSupport) \
.filter_by(package=package, confidence=1) \
.delete()
# Add new
supported_games = gs_package.supported_games \
.difference(gs_package.user_supported_games)
for game_name in supported_games:
game_id = session.query(Package.id) \
.filter(Package.type == PackageType.GAME, Package.name == game_name, Package.state == PackageState.APPROVED) \
.one()[0]
new_support = PackageGameSupport()
new_support.package = package
new_support.game_id = game_id
new_support.confidence = 1
new_support.supports = True
session.add(new_support)
def game_support_update(session: sqlalchemy.orm.Session, package: Package, old_provides: Optional[set[str]]) -> set[str]:
support = _create_instance(session)
gs_package = support.get(package.get_id())
if gs_package is None:
gs_package = _convert_package(support, package)
support.on_update(gs_package, old_provides)
_persist(session, support)
return gs_package.errors
def game_support_update_all(session: sqlalchemy.orm.Session):
support = _create_instance(session)
support.on_first_run()
_persist(session, support)
def game_support_remove(session: sqlalchemy.orm.Session, package: Package):
support = _create_instance(session)
gs_package = support.get(package.get_id())
if gs_package is None:
gs_package = _convert_package(support, package)
support.on_remove(gs_package)
_persist(session, support)
def game_support_set(session, package: Package, game_is_supported: Dict[int, bool], confidence: int):
previous_supported: Dict[int, PackageGameSupport] = {}
for support in package.supported_games.all():
previous_supported[support.game.id] = support
for game_id, supports in game_is_supported.items():
game = session.query(Package).get(game_id)
lookup = previous_supported.pop(game_id, None)
if lookup is None:
support = PackageGameSupport()
support.package = package
support.game = game
support.confidence = confidence
support.supports = supports
session.add(support)
elif lookup.confidence <= confidence:
lookup.supports = supports
lookup.confidence = confidence
for game, support in previous_supported.items():
if support.confidence == confidence:
session.delete(support)

166
app/logic/graphs.py Normal file
View File

@@ -0,0 +1,166 @@
# 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 datetime
from datetime import timedelta
from typing import Optional
from app.models import User, Package, PackageDailyStats, db, PackageState
from sqlalchemy import func
def daterange(start_date, end_date):
for n in range(int((end_date - start_date).days) + 1):
yield start_date + timedelta(n)
keys = ["platform_minetest", "platform_other", "reason_new",
"reason_dependency", "reason_update"]
def flatten_data(stats):
start_date = stats[0].date
end_date = stats[-1].date
result = {
"start": start_date.isoformat(),
"end": end_date.isoformat(),
}
for key in keys:
result[key] = []
i = 0
for date in daterange(start_date, end_date):
stat = stats[i]
if stat.date == date:
for key in keys:
result[key].append(getattr(stat, key))
i += 1
else:
for key in keys:
result[key].append(0)
return result
def get_package_stats(package: Package, start_date: Optional[datetime.date], end_date: Optional[datetime.date]):
query = package.daily_stats.order_by(db.asc(PackageDailyStats.date))
if start_date:
query = query.filter(PackageDailyStats.date >= start_date)
if end_date:
query = query.filter(PackageDailyStats.date <= end_date)
stats = query.all()
if len(stats) == 0:
return None
return flatten_data(stats)
def get_package_stats_for_user(user: User, start_date: Optional[datetime.date], end_date: Optional[datetime.date]):
query = db.session \
.query(PackageDailyStats.date,
func.sum(PackageDailyStats.platform_minetest).label("platform_minetest"),
func.sum(PackageDailyStats.platform_other).label("platform_other"),
func.sum(PackageDailyStats.reason_new).label("reason_new"),
func.sum(PackageDailyStats.reason_dependency).label("reason_dependency"),
func.sum(PackageDailyStats.reason_update).label("reason_update")) \
.filter(PackageDailyStats.package.has(author_id=user.id))
if start_date:
query = query.filter(PackageDailyStats.date >= start_date)
if end_date:
query = query.filter(PackageDailyStats.date <= end_date)
stats = query.order_by(db.asc(PackageDailyStats.date)) \
.group_by(PackageDailyStats.date) \
.all()
if len(stats) == 0:
return None
results = flatten_data(stats)
results["package_downloads"] = get_package_overview_for_user(user, stats[0].date, stats[-1].date)
return results
def get_package_overview_for_user(user: Optional[User], start_date: datetime.date, end_date: datetime.date):
query = db.session \
.query(PackageDailyStats.package_id, PackageDailyStats.date,
(PackageDailyStats.platform_minetest + PackageDailyStats.platform_other).label("downloads"))
if user:
query = query.filter(PackageDailyStats.package.has(author_id=user.id))
all_stats = query \
.filter(PackageDailyStats.package.has(state=PackageState.APPROVED),
PackageDailyStats.date >= start_date, PackageDailyStats.date <= end_date) \
.order_by(db.asc(PackageDailyStats.package_id), db.asc(PackageDailyStats.date)) \
.all()
stats_by_package = {}
for stat in all_stats:
bucket = stats_by_package.get(stat.package_id, [])
stats_by_package[stat.package_id] = bucket
bucket.append(stat)
package_title_by_id = {}
pkg_query = user.packages if user else Package.query
for package in pkg_query.filter_by(state=PackageState.APPROVED).all():
if user:
package_title_by_id[package.id] = package.title
else:
package_title_by_id[package.id] = package.get_id()
result = {}
for package_id, stats in stats_by_package.items():
i = 0
row = []
result[package_title_by_id[package_id]] = row
for date in daterange(start_date, end_date):
if i >= len(stats):
row.append(0)
continue
stat = stats[i]
if stat.date == date:
row.append(stat.downloads)
i += 1
elif stat.date > date:
row.append(0)
else:
raise Exception(f"Invalid logic, expected stat {stat.date} to be later than {date}")
return result
def get_all_package_stats(start_date: Optional[datetime.date] = None, end_date: Optional[datetime.date] = None):
now_date = datetime.datetime.utcnow().date()
if end_date is None or end_date > now_date:
end_date = now_date
min_start_date = (datetime.datetime.utcnow() - datetime.timedelta(days=29)).date()
if start_date is None or start_date < min_start_date:
start_date = min_start_date
return {
"start": start_date.isoformat(),
"end": end_date.isoformat(),
"package_downloads": get_package_overview_for_user(None, start_date, end_date),
}

View File

@@ -0,0 +1,204 @@
# 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 List, Tuple, Union, Optional
from flask_babel import lazy_gettext, LazyString
from sqlalchemy import and_, or_
from app.models import Package, PackageType, PackageState, PackageRelease, db, MetaPackage, ForumTopic, User, \
Permission, UserRank
class PackageValidationNote:
# level is danger, warning, or info
level: str
message: LazyString
buttons: List[Tuple[str, LazyString]]
# False to prevent "Approve"
allow_approval: bool
# False to prevent "Submit for Approval"
allow_submit: bool
def __init__(self, level: str, message: LazyString, allow_approval: bool, allow_submit: bool):
self.level = level
self.message = message
self.buttons = []
self.allow_approval = allow_approval
self.allow_submit = allow_submit
def add_button(self, url: str, label: LazyString) -> "PackageValidationNote":
self.buttons.append((url, label))
return self
def __repr__(self):
return str(self.message)
def is_package_name_taken(normalised_name: str) -> bool:
return Package.query.filter(
and_(Package.state == PackageState.APPROVED,
or_(Package.name == normalised_name,
Package.name == normalised_name + "_game"))).count() > 0
def get_conflicting_mod_names(package: Package) -> set[str]:
conflicting_modnames = (db.session.query(MetaPackage.name)
.filter(MetaPackage.id.in_([mp.id for mp in package.provides]))
.filter(MetaPackage.packages.any(and_(Package.id != package.id, Package.state == PackageState.APPROVED)))
.all())
conflicting_modnames += (db.session.query(ForumTopic.name)
.filter(ForumTopic.name.in_([mp.name for mp in package.provides]))
.filter(ForumTopic.topic_id != package.forums)
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id))
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title))
.all())
return set([x[0] for x in conflicting_modnames])
def count_packages_with_forum_topic(topic_id: int) -> int:
return Package.query.filter(Package.forums == topic_id, Package.state != PackageState.DELETED).count() > 1
def get_forum_topic(topic_id: int) -> Optional[ForumTopic]:
return ForumTopic.query.get(topic_id)
def validate_package_for_approval(package: Package) -> List[PackageValidationNote]:
retval: List[PackageValidationNote] = []
def template(level: str, allow_approval: bool, allow_submit: bool):
def inner(msg: LazyString):
note = PackageValidationNote(level, msg, allow_approval, allow_submit)
retval.append(note)
return note
return inner
danger = template("danger", allow_approval=False, allow_submit=False)
warning = template("warning", allow_approval=True, allow_submit=True)
info = template("info", allow_approval=False, allow_submit=True)
if package.type != PackageType.MOD and is_package_name_taken(package.normalised_name):
danger(lazy_gettext("A package already exists with this name. Please see Policy and Guidance 3"))
if package.releases.filter(PackageRelease.task_id.is_(None)).count() == 0:
if package.releases.count() == 0:
message = lazy_gettext("You need to create a release before this package can be approved.")
else:
message = lazy_gettext("Release is still importing, or has an error.")
danger(message) \
.add_button(package.get_url("packages.create_release"), lazy_gettext("Create release")) \
.add_button(package.get_url("packages.setup_releases"), lazy_gettext("Set up releases"))
# Don't bother validating any more until we have a release
return retval
if package.screenshots.count() == 0:
danger(lazy_gettext("You need to add at least one screenshot."))
missing_deps = package.get_missing_hard_dependencies_query().all()
if len(missing_deps) > 0:
missing_deps = ", ".join([ x.name for x in missing_deps])
danger(lazy_gettext(
"The following hard dependencies need to be added to ContentDB first: %(deps)s", deps=missing_deps))
if package.type != PackageType.GAME and not package.supports_all_games and package.supported_games.count() == 0:
danger(lazy_gettext(
"What games does your package support? Please specify on the supported games page", deps=missing_deps)) \
.add_button(package.get_url("packages.game_support"), lazy_gettext("Supported Games"))
if "Other" in package.license.name or "Other" in package.media_license.name:
info(lazy_gettext("Please wait for the license to be added to CDB."))
# Check similar mod name
conflicting_modnames = set()
if package.type != PackageType.TXP:
conflicting_modnames = get_conflicting_mod_names(package)
if len(conflicting_modnames) > 4:
warning(lazy_gettext("Please make sure that this package has the right to the names it uses."))
elif len(conflicting_modnames) > 0:
names_list = list(conflicting_modnames)
names_list.sort()
warning(lazy_gettext("Please make sure that this package has the right to the names %(names)s",
names=", ".join(names_list))) \
.add_button(package.get_url('packages.similar'), lazy_gettext("See more"))
# Check forum topic
if package.state != PackageState.APPROVED and package.forums is not None:
if count_packages_with_forum_topic(package.forums) > 1:
danger("<b>" + lazy_gettext("Error: Another package already uses this forum topic!") + "</b>")
topic = get_forum_topic(package.forums)
if topic is not None:
if topic.author != package.author:
danger("<b>" + lazy_gettext("Error: Forum topic author doesn't match package author.") + "</b>")
elif package.type != PackageType.TXP:
warning(lazy_gettext("Warning: Forum topic not found. The topic may have been created since the last forum crawl."))
return retval
PACKAGE_STATE_FLOW = {
PackageState.WIP: {PackageState.READY_FOR_REVIEW},
PackageState.CHANGES_NEEDED: {PackageState.READY_FOR_REVIEW},
PackageState.READY_FOR_REVIEW: {PackageState.WIP, PackageState.CHANGES_NEEDED, PackageState.APPROVED},
PackageState.APPROVED: {PackageState.CHANGES_NEEDED},
PackageState.DELETED: {PackageState.READY_FOR_REVIEW},
}
def can_move_to_state(package: Package, user: User, new_state: Union[str, PackageState]) -> bool:
if not user.is_authenticated:
return False
if type(new_state) == str:
new_state = PackageState[new_state]
elif type(new_state) != PackageState:
raise Exception("Unknown state given to can_move_to_state()")
if new_state not in PACKAGE_STATE_FLOW[package.state]:
return False
if new_state == PackageState.READY_FOR_REVIEW or new_state == PackageState.APPROVED:
# Can the user approve?
if new_state == PackageState.APPROVED and not package.check_perm(user, Permission.APPROVE_NEW):
return False
# Must be able to edit or approve package to change its state
if not (package.check_perm(user, Permission.APPROVE_NEW) or package.check_perm(user, Permission.EDIT_PACKAGE)):
return False
# Are there any validation warnings?
validation_notes = validate_package_for_approval(package)
for note in validation_notes:
if not note.allow_submit or (new_state == PackageState.APPROVED and not note.allow_approval):
return False
return True
elif new_state == PackageState.CHANGES_NEEDED:
return package.check_perm(user, Permission.APPROVE_NEW)
elif new_state == PackageState.WIP:
return package.check_perm(user, Permission.EDIT_PACKAGE) and \
(user in package.maintainers or user.rank.at_least(UserRank.ADMIN))
return True

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