├── app ├── tests │ ├── integ │ │ ├── __init__.py │ │ └── test_homepage.py │ ├── unit │ │ ├── minetestcheck │ │ │ ├── no_textdomain_comment.fr.tr │ │ │ ├── bad_args.fr.tr │ │ │ ├── bad_escape.fr.tr │ │ │ ├── unknown_arg.fr.tr │ │ │ ├── textdomain_mismatch.fr.tr │ │ │ ├── err_missing_eq.fr.tr │ │ │ ├── __init__.py │ │ │ └── foo.bar.fr.tr │ │ ├── __init__.py │ │ ├── logic │ │ │ ├── __init__.py │ │ │ └── test_graphs.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── test_url.py │ │ │ └── test_utils.py │ │ └── markdown │ │ │ └── test_markdown.py │ └── __init__.py ├── public │ ├── favicon-128.png │ ├── favicon-16.png │ ├── favicon-32.png │ ├── static │ │ ├── puzzle.png │ │ ├── gamejam.png │ │ ├── bot_avatar.png │ │ ├── placeholder.png │ │ ├── installing_cdb_dialog.png │ │ ├── contentdb_flag_blacklist.png │ │ ├── fa │ │ │ ├── webfonts │ │ │ │ ├── fa-solid-900.eot │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-brands-400.eot │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff │ │ │ │ ├── fa-regular-400.eot │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-solid-900.woff │ │ │ │ ├── fa-solid-900.woff2 │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.woff │ │ │ │ └── fa-regular-400.woff2 │ │ │ └── css │ │ │ │ ├── brands.min.css │ │ │ │ ├── solid.min.css │ │ │ │ ├── regular.min.css │ │ │ │ ├── brands.css │ │ │ │ ├── solid.css │ │ │ │ └── regular.css │ │ ├── installing_content_tab.png │ │ ├── installing_select_mods.png │ │ ├── minetest_client_buttons.png │ │ ├── fonts │ │ │ ├── lato-v24-latin_latin-ext-700.woff2 │ │ │ ├── lato-v24-latin_latin-ext-italic.woff2 │ │ │ └── lato-v24-latin_latin-ext-regular.woff2 │ │ ├── libs │ │ │ └── images │ │ │ │ ├── ui-icons_00498f_256x240.png │ │ │ │ ├── ui-icons_98d2fb_256x240.png │ │ │ │ ├── ui-icons_9ccdfc_256x240.png │ │ │ │ └── ui-icons_ffffff_256x240.png │ │ ├── js │ │ │ ├── email_disable_all.js │ │ │ ├── gallery_carousel.js │ │ │ ├── screenshots_editor.js │ │ │ ├── release_new.js │ │ │ ├── release_bulk_change.js │ │ │ ├── release_minmax.js │ │ │ └── video_embed.js │ │ └── opensearch.xml │ └── robots.txt ├── logic │ ├── __init__.py │ └── LogicError.py ├── blueprints │ ├── todo │ │ └── __init__.py │ ├── vcs │ │ └── __init__.py │ ├── users │ │ └── __init__.py │ ├── admin │ │ ├── __init__.py │ │ └── update_config.py │ ├── __init__.py │ ├── api │ │ ├── auth.py │ │ └── __init__.py │ └── translate │ │ └── __init__.py ├── utils │ ├── image.py │ ├── gravatar.py │ └── url.py ├── templates │ ├── 500.html │ ├── flask_user_layout.html │ ├── 404.html │ ├── admin │ │ ├── switch_user.html │ │ ├── versions │ │ │ ├── list.html │ │ │ └── edit.html │ │ ├── audit_view.html │ │ ├── send_bulk_notification.html │ │ ├── transfer.html │ │ ├── licenses │ │ │ ├── list.html │ │ │ └── edit.html │ │ ├── audit.html │ │ ├── languages │ │ │ └── edit.html │ │ ├── warnings │ │ │ ├── edit.html │ │ │ └── list.html │ │ ├── tags │ │ │ └── edit.html │ │ ├── restore.html │ │ └── update_config.html │ ├── packages │ │ ├── audit.html │ │ ├── user_screenshot_new.html │ │ ├── reviews_list.html │ │ ├── alias_create_edit.html │ │ ├── screenshot_new.html │ │ ├── share.html │ │ ├── edit_maintainers.html │ │ ├── alias_list.html │ │ ├── stats.html │ │ ├── screenshot_edit.html │ │ ├── package_base.html │ │ └── advanced_search.html │ ├── emails │ │ ├── unable_to_find_account.html │ │ ├── verify_unsubscribe.html │ │ ├── verify.html │ │ └── notification.html │ ├── users │ │ ├── forums_no_such_user.html │ │ ├── email_sent.html │ │ ├── forgot_password.html │ │ ├── stats.html │ │ ├── claim.html │ │ ├── settings_base.html │ │ └── change_set_password.html │ ├── todo │ │ ├── modnames.html │ │ ├── outdated.html │ │ └── mtver_support.html │ ├── threads │ │ ├── edit_reply.html │ │ ├── list.html │ │ ├── delete_reply.html │ │ └── delete_thread.html │ ├── collections │ │ └── delete.html │ ├── report │ │ ├── edit.html │ │ ├── list.html │ │ └── report_received.html │ ├── tasks │ │ └── view.html │ ├── modnames │ │ ├── list.html │ │ └── view.html │ ├── api │ │ └── list_tokens.html │ ├── oauth │ │ └── list_clients.html │ ├── zipgrep │ │ └── search.html │ └── macros │ │ └── pagination.html ├── flatpages │ ├── help │ │ ├── contact_us.md │ │ ├── feeds.md │ │ ├── metrics.md │ │ └── wtfpl.md │ └── help.md ├── tasks │ └── admintasks.py ├── scss │ ├── comments.scss │ └── lato.scss └── rediscache.py ├── migrations ├── README ├── versions │ ├── 663521dfe86d_.py │ ├── 6e57b2b4dcdf_.py │ ├── c5e4213721dd_.py │ ├── 8f55dfbec825_.py │ ├── aa53b4d36c50_.py │ ├── c154912eaa0c_.py │ ├── 57b7fbc174cf_.py │ ├── 23afcf580aae_.py │ ├── a791b9b74a4c_.py │ ├── 64fee8e5ab34_.py │ ├── 7a48dbd05780_.py │ ├── 8807a5279793_.py │ ├── f94192c54b73_.py │ ├── 011e42c52d21_.py │ ├── 28a427cbd4cf_.py │ ├── 7f166b5218d7_.py │ ├── 1af840af0209_.py │ ├── 306ce331a2a7_.py │ ├── 4585ce5147b8_.py │ ├── e82c2141fae3_.py │ ├── 96811eb565c1_.py │ ├── 8425c06b7d77_.py │ ├── 105d4c740ad6_.py │ ├── c141a63b2487_.py │ ├── e02ce7e98d92_.py │ ├── 43dc7dbf64c8_.py │ ├── df66c78e6791_.py │ ├── 20f2aa2f40b9_.py │ ├── 2ecff2f9972d_.py │ ├── 9395ba96f853_.py │ ├── e1bf78a597a2_.py │ ├── ea5a023711e0_.py │ ├── 06af23184d15_.py │ ├── 49105d276908_.py │ ├── b254f55eadd2_.py │ ├── 1acc6e90bbac_.py │ ├── 6e59ad5cc62a_.py │ ├── dce69ad1e4eb_.py │ ├── 11b6ef362f98_.py │ ├── 7a749a6c8c3a_.py │ ├── 86512692b770_.py │ ├── 8679442b8dde_.py │ ├── c181c6c88bae_.py │ ├── 9e2ac631efb0_.py │ ├── d0bec9e5698e_.py │ ├── 3a24fc02365e_.py │ ├── a0f6c8743362_.py │ ├── 97a9c461bc2d_.py │ ├── 9ec17b558413_.py │ ├── c4152f4240ed_.py │ ├── f6ef5f35abca_.py │ ├── 76ff303f76d8_.py │ ├── fa12fadbdb40_.py │ ├── 13113e5710da_.py │ ├── 44e138485931_.py │ ├── 6dca6eceb04d_.py │ ├── 725ff70ea316_.py │ ├── d4262fb15b37_.py │ ├── 3f4d7cd8401f_.py │ ├── 019da77ba02d_.py │ ├── 96a01fe23389_.py │ ├── dd17239f7144_.py │ ├── ead35f7d446c_.py │ ├── 1fe2e44cf565_.py │ ├── dd27f1311a90_.py │ ├── d6ae9682c45f_.py │ ├── dff4b87e4a76_.py │ ├── 242fd82077bb_.py │ ├── 17b303f33f68_.py │ ├── 838081950f27_.py │ ├── 9689a71efe88_.py │ ├── 16eb610b7751_.py │ ├── 8d22def23c8b_.py │ ├── 8ee3cf3fb312_.py │ ├── aa6d21889d22_.py │ ├── de004661c5e1_.py │ ├── 01f8d5de29e1_.py │ ├── 1e08d7e4c15d_.py │ ├── 81e0eb07a3cd_.py │ ├── 9832944cd1e4_.py │ ├── e9f534df23a8_.py │ ├── f30031f0b928_.py │ ├── 06d23947e7ef_.py │ ├── 7828535fe339_.py │ ├── 3052712496e4_.py │ ├── e571b3498f9e_.py │ ├── 51be0401bb85_.py │ ├── ea83ce985e55_.py │ ├── f565dde93553_.py │ ├── 3f5836a3df5c_.py │ ├── 81de25b72f66_.py │ ├── dabd7ab14339_.py │ ├── fd25bf3e57c3_.py │ ├── 886c92dc6eaa_.py │ ├── 097ce5d114d9_.py │ ├── a337bcc165c0_.py │ ├── adad68a5e370_.py │ ├── cb6ab141c522_.py │ ├── aa6d7b595a94_.py │ ├── daa040b727b2_.py │ └── 2f3c3597c78d_.py ├── script.py.mako └── alembic.ini ├── babel.cfg ├── .dockerignore ├── utils ├── common.sh ├── ci │ ├── config.env │ └── config.cfg ├── reload_worker.sh ├── bash.sh ├── db.sh ├── reload.sh ├── worker_clear.sh ├── tests_cov.sh ├── run_migrations.sh ├── tests.sh ├── downgrade_migration.sh ├── start.sh ├── update.sh ├── create_migration.sh ├── entrypoint.sh ├── restore_bk.sh └── setup.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── policy.md │ ├── bug_report.md │ └── feature_request.md ├── SECURITY.md └── workflows │ └── test.yml ├── docs └── README.md ├── Dockerfile ├── requirements.txt └── config.example.cfg /app/tests/integ/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: app/**.py] 2 | [jinja2: app/templates/**.html] 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | data* 3 | uploads 4 | **/*.pyc 5 | **/__pycache__ 6 | env 7 | venv 8 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/no_textdomain_comment.fr.tr: -------------------------------------------------------------------------------- 1 | Hello, World! = Bonjour, Monde! 2 | -------------------------------------------------------------------------------- /utils/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | container() { 6 | echo "contentdb-$1-1" 7 | } 8 | -------------------------------------------------------------------------------- /app/public/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/favicon-128.png -------------------------------------------------------------------------------- /app/public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/favicon-16.png -------------------------------------------------------------------------------- /app/public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/favicon-32.png -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/bad_args.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: bad_args 2 | Some @1 args @5=Some @1 args @5 3 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/bad_escape.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: bad_escape 2 | Bad @x escape = Bad @x escape 3 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/unknown_arg.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: unknown_arg 2 | Some @1 arg=Some @2 arg 3 | -------------------------------------------------------------------------------- /app/public/static/puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/puzzle.png -------------------------------------------------------------------------------- /app/public/static/gamejam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/gamejam.png -------------------------------------------------------------------------------- /app/public/static/bot_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/bot_avatar.png -------------------------------------------------------------------------------- /app/public/static/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/placeholder.png -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/textdomain_mismatch.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: foobar 2 | 3 | Hello, World! = Bonjour, Monde! 4 | -------------------------------------------------------------------------------- /utils/ci/config.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=contentdb 2 | POSTGRES_PASSWORD=password 3 | POSTGRES_DB=contentdb 4 | FLASK_DEBUG=1 5 | -------------------------------------------------------------------------------- /utils/reload_worker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | docker compose build worker 6 | docker compose up --no-deps -d worker 7 | -------------------------------------------------------------------------------- /app/logic/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /app/public/static/installing_cdb_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/installing_cdb_dialog.png -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/err_missing_eq.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: err_missing_eq 2 | 3 | Hello, World! = Bonjour, Monde! 4 | Invalid line 5 | -------------------------------------------------------------------------------- /app/public/static/contentdb_flag_blacklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/contentdb_flag_blacklist.png -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /app/public/static/installing_content_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/installing_content_tab.png -------------------------------------------------------------------------------- /app/public/static/installing_select_mods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/installing_select_mods.png -------------------------------------------------------------------------------- /app/public/static/minetest_client_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/minetest_client_buttons.png -------------------------------------------------------------------------------- /app/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /packages/*/*/download/ 3 | Disallow: /packages/*/*/releases/*/download/ 4 | Disallow: /report/ 5 | -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /app/public/static/fa/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fa/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/tests/unit/logic/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /app/tests/unit/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | -------------------------------------------------------------------------------- /utils/bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Open SSH to app instance 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec -it "$(container app)" bash 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | liberapay: rubenwardy 4 | patreon: rubenwardy 5 | custom: [ "https://rubenwardy.com/donate/" ] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/policy.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Policy suggestion 3 | about: Suggest a change to the guidelines 4 | title: '' 5 | labels: Policy 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /app/public/static/fonts/lato-v24-latin_latin-ext-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fonts/lato-v24-latin_latin-ext-700.woff2 -------------------------------------------------------------------------------- /app/public/static/libs/images/ui-icons_00498f_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/libs/images/ui-icons_00498f_256x240.png -------------------------------------------------------------------------------- /app/public/static/libs/images/ui-icons_98d2fb_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/libs/images/ui-icons_98d2fb_256x240.png -------------------------------------------------------------------------------- /app/public/static/libs/images/ui-icons_9ccdfc_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/libs/images/ui-icons_9ccdfc_256x240.png -------------------------------------------------------------------------------- /app/public/static/libs/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/libs/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /app/public/static/fonts/lato-v24-latin_latin-ext-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fonts/lato-v24-latin_latin-ext-italic.woff2 -------------------------------------------------------------------------------- /app/public/static/fonts/lato-v24-latin_latin-ext-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luanti-org/contentdb/HEAD/app/public/static/fonts/lato-v24-latin_latin-ext-regular.woff2 -------------------------------------------------------------------------------- /utils/db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Open SQL console for the database 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec -it "$(container db)" psql contentdb contentdb 9 | -------------------------------------------------------------------------------- /utils/reload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Hot/live reload - only works in debug mode 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec "$(container app)" sh -c "cp -r /source/* ." 9 | -------------------------------------------------------------------------------- /utils/worker_clear.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Clear worker queue 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec -it "$(container app)" sh -c "FLASK_CONFIG=../config.cfg celery -A app.tasks.celery purge -f" 9 | -------------------------------------------------------------------------------- /utils/tests_cov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | . "${BASH_SOURCE%/*}/common.sh" 5 | 6 | docker exec "$(container app)" sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py python -m pytest app/tests/ --cov=app --disable-warnings" 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | This folder only contains technical documentation for those interested in the ContentDB source code. 4 | 5 | Documentation for using ContentDB, whether through the interface or API, is available at 6 | . 7 | -------------------------------------------------------------------------------- /app/tests/unit/minetestcheck/foo.bar.fr.tr: -------------------------------------------------------------------------------- 1 | # textdomain: foo.bar 2 | 3 | Hello, World! = Bonjour, Monde! 4 | Hello @1!=@1, salut! 5 | Cats @= cool = Chats = cool 6 | # a comment 7 | A @n newline = Une @ 8 | nouvelle ligne 9 | Maybe @@@n@@@=@@= Peut être @@@n@@@=@@ 10 | -------------------------------------------------------------------------------- /utils/run_migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run all pending migrations 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | ./utils/reload.sh 9 | docker exec "$(container app)" sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade" 10 | -------------------------------------------------------------------------------- /utils/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | . "${BASH_SOURCE%/*}/common.sh" 5 | 6 | # To do a specific test file, change the path 7 | docker exec "$(container app)" sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py python -m pytest app/tests/ --disable-warnings" 8 | -------------------------------------------------------------------------------- /app/blueprints/todo/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | from flask import Blueprint 7 | 8 | bp = Blueprint("todo", __name__) 9 | 10 | from . import editor, user 11 | -------------------------------------------------------------------------------- /app/blueprints/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | from flask import Blueprint 7 | 8 | bp = Blueprint("vcs", __name__) 9 | 10 | from . import github, gitlab 11 | -------------------------------------------------------------------------------- /utils/downgrade_migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create a database migration, and copy it back to the host. 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec "$(container app)" sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db downgrade" 9 | -------------------------------------------------------------------------------- /app/blueprints/users/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from flask import Blueprint 6 | 7 | bp = Blueprint("users", __name__) 8 | 9 | from . import profile, claim, account, settings 10 | -------------------------------------------------------------------------------- /utils/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Call from a docker host to build and start CDB. 5 | # This is really only for production mode, for debugging it's better to use 6 | # docker compose directly: docker compose up --build 7 | # 8 | 9 | docker compose up --build -d --scale worker=4 10 | -------------------------------------------------------------------------------- /app/utils/image.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | from typing import Tuple 7 | from PIL import Image 8 | 9 | 10 | def get_image_size(path: str) -> Tuple[int,int]: 11 | im = Image.open(path) 12 | return im.size 13 | -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | 500 - Internal Server Error 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{ self.title() }}

9 |

10 | An error has occurred whilst loading this page. The ContentDB team has been notified. 11 |

12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Unconfirmed Bug 6 | assignees: '' 7 | --- 8 | 9 | ## Summary 10 | Describe your problem here 11 | 12 | ##### Steps to reproduce 13 | For bug reports or build issues, explain how the problem happened 14 | -------------------------------------------------------------------------------- /app/utils/gravatar.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def get_gravatar(email: str): 5 | size = 64 6 | rating = "g" 7 | default = "retro" 8 | url = "https://secure.gravatar.com/avatar/" 9 | email_hash = hashlib.md5(email.encode("utf-8")).hexdigest() 10 | link = f"{url}{email_hash}?s={size}&d={default}&r={rating}" 11 | return link 12 | -------------------------------------------------------------------------------- /app/templates/flask_user_layout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block container %} 4 |
5 |
6 | 7 |
8 | {% block content %} 9 | {% endblock %} 10 |
11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Page not found") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{ self.title() }}

9 |

10 | {{ _("That page could not be found. The link may be broken, the page may have been deleted, or you may not have access to it.") }} 11 |

12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /app/logic/LogicError.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | class LogicError(Exception): 7 | def __init__(self, code, message): 8 | self.code = code 9 | self.message = message 10 | 11 | def __str__(self): 12 | return repr("LogicError {}: {}".format(self.code, self.message)) 13 | -------------------------------------------------------------------------------- /app/blueprints/admin/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | from flask import Blueprint 7 | 8 | bp = Blueprint("admin", __name__) 9 | 10 | from . import admin, audit, licenseseditor, tagseditor, versioneditor, warningseditor, languageseditor, approval_stats, update_config, usereditor 11 | -------------------------------------------------------------------------------- /app/tests/unit/markdown/test_markdown.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from app.markdown import render_markdown 6 | 7 | def test_linkify(): 8 | assert render_markdown("hello readme.md https://readme.md") == "

hello readme.md https://readme.md

\n" 9 | -------------------------------------------------------------------------------- /utils/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Call from a docker host to rebuild and update running instances of CDB. 5 | # This is for production use. See reload.sh for debug mode hot/live reloading. 6 | # 7 | 8 | set -e 9 | 10 | docker compose build app 11 | docker compose build worker 12 | 13 | docker compose up --no-deps -d app 14 | docker compose up --no-deps --scale worker=4 -d worker 15 | -------------------------------------------------------------------------------- /app/flatpages/help/contact_us.md: -------------------------------------------------------------------------------- 1 | title: Contact Us 2 | 3 | ## Reports 4 | 5 | Please let us know if anything on the ContentDB violates our rules or any applicable 6 | laws. 7 | 8 | We take copyright violation and other offenses very seriously. 9 | 10 | Report 11 | 12 | ## Other 13 | 14 | Contact the admin 15 | -------------------------------------------------------------------------------- /app/public/static/js/email_disable_all.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | const disableAll = document.getElementById("disable-all"); 7 | disableAll.classList.remove("d-none"); 8 | disableAll.addEventListener("click", () => { 9 | document.querySelectorAll("input[type='checkbox']").forEach(x => { x.checked = false; }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/templates/admin/switch_user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Switch User 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Log in as another user

9 | 10 | {% from "macros/forms.html" import render_field, render_submit_field %} 11 |
12 | {{ form.hidden_tag() }} 13 | 14 | {{ render_field(form.username) }} 15 | {{ render_submit_field(form.submit) }} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /utils/create_migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create a database migration, and copy it back to the host. 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | docker exec "$(container app)" sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate" 9 | docker exec -u root "$(container app)" sh -c "cp /home/cdb/migrations/versions/* /source/migrations/versions/" 10 | 11 | USER=$(whoami) 12 | sudo chown -R "$USER:$USER" migrations/versions 13 | -------------------------------------------------------------------------------- /app/tasks/admintasks.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from . import celery 6 | from app.models import db, Thread 7 | 8 | 9 | @celery.task 10 | def delete_empty_threads(): 11 | query = Thread.query.filter(~Thread.replies.any()) 12 | count = query.count() 13 | for thread in query.all(): 14 | thread.watchers.clear() 15 | db.session.delete(thread) 16 | db.session.commit() 17 | -------------------------------------------------------------------------------- /migrations/versions/663521dfe86d_.py: -------------------------------------------------------------------------------- 1 | from alembic import op 2 | import sqlalchemy as sa 3 | from sqlalchemy.dialects import postgresql 4 | 5 | # revision identifiers, used by Alembic. 6 | revision = '663521dfe86d' 7 | down_revision = 'c181c6c88bae' 8 | branch_labels = None 9 | depends_on = None 10 | 11 | 12 | def upgrade(): 13 | op.rename_table("minetest_release", "luanti_release") 14 | 15 | 16 | def downgrade(): 17 | op.rename_table("luanti_release", "minetest_release") 18 | -------------------------------------------------------------------------------- /app/public/static/js/gallery_carousel.js: -------------------------------------------------------------------------------- 1 | // @author recluse4615 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | const galleryCarousel = new bootstrap.Carousel(document.getElementById("galleryCarousel")); 7 | document.querySelectorAll(".gallery-image").forEach(el => { 8 | el.addEventListener("click", function(e) { 9 | galleryCarousel.to(el.dataset.bsSlideTo); 10 | e.preventDefault(); 11 | }); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /app/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | import importlib 6 | import os 7 | 8 | 9 | def create_blueprints(app): 10 | dir = os.path.dirname(os.path.realpath(__file__)) 11 | modules = next(os.walk(dir))[1] 12 | 13 | for modname in modules: 14 | if all(c.islower() for c in modname): 15 | module = importlib.import_module("." + modname, __name__) 16 | app.register_blueprint(module.bp) 17 | -------------------------------------------------------------------------------- /app/public/static/opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ContentDB 4 | ContentDB 5 | UTF-8 6 | Search mods, games, and textures for Luanti. 7 | Luanti Minetest Mod Game Subgame Search 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/templates/packages/audit.html: -------------------------------------------------------------------------------- 1 | {% extends "packages/package_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Audit Log") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{ self.title() }}

9 | 10 | {% from "macros/pagination.html" import render_pagination %} 11 | {% from "macros/audit_log.html" import render_audit_log %} 12 | 13 | {{ render_pagination(pagination, url_set_query) }} 14 | {{ render_audit_log(log, current_user) }} 15 | {{ render_pagination(pagination, url_set_query) }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /app/public/static/js/screenshots_editor.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | window.addEventListener("load", () => { 7 | function update() { 8 | const elements = [...document.querySelector(".sortable").children]; 9 | const ids = elements.map(x => x.dataset.id).filter(x => x); 10 | document.querySelector("input[name='order']").value = ids.join(","); 11 | } 12 | 13 | update(); 14 | $(".sortable").sortable({ 15 | update: update 16 | }); 17 | }) 18 | -------------------------------------------------------------------------------- /migrations/versions/6e57b2b4dcdf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6e57b2b4dcdf 4 | Revises: 17b303f33f68 5 | Create Date: 2022-01-22 20:35:25.494712 6 | """ 7 | 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6e57b2b4dcdf' 14 | down_revision = '17b303f33f68' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('user', sa.Column('locale', sa.String(length=10), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('user', 'locale') 25 | -------------------------------------------------------------------------------- /migrations/versions/c5e4213721dd_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c5e4213721dd 4 | Revises: 9832944cd1e4 5 | Create Date: 2020-07-15 17:54:33.738132 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c5e4213721dd' 14 | down_revision = '9832944cd1e4' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('tag', sa.Column('views', sa.Integer(), nullable=False, server_default="0")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('tag', 'views') 25 | -------------------------------------------------------------------------------- /app/templates/emails/unable_to_find_account.html: -------------------------------------------------------------------------------- 1 |

2 | {{ _("We were unable to perform the password reset as we could not find an account associated with this email.") }} 3 |

4 |

5 | {{ _("This may be because you used another email with your account, or because you never confirmed your email.") }} 6 |

7 |

8 | {{ _("You can use GitHub to log in if it is associated with your account.") }} 9 | {{ _("Otherwise, you may need to contact the admin for help.") }} 10 |

11 |

12 | {{ _("If you weren't expecting to receive this email, then you can safely ignore it.") }} 13 |

14 | -------------------------------------------------------------------------------- /app/templates/users/forums_no_such_user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ username }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

10 | {{ username }} 11 |

12 | 13 |

14 | {{ _("Unfortunately, %(username)s doesn't have an account on ContentDB yet.", username=username) }} 15 |

16 | 17 | {% if not current_user.is_authenticated %} 18 | 19 | {{ _("Claim Account") }} 20 | 21 | {% endif %} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /migrations/versions/8f55dfbec825_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8f55dfbec825 4 | Revises: 242fd82077bb 5 | Create Date: 2025-09-23 15:21:06.445012 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '8f55dfbec825' 13 | down_revision = '242fd82077bb' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.execute(text("COMMIT")) 20 | op.execute(text("ALTER TYPE reportcategory ADD VALUE 'SPAM' AFTER 'USER_CONDUCT'")) 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /migrations/versions/aa53b4d36c50_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: aa53b4d36c50 4 | Revises: ea83ce985e55 5 | Create Date: 2023-03-05 18:11:29.743388 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'aa53b4d36c50' 13 | down_revision = 'ea83ce985e55' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.add_column('package', sa.Column('donate_url', sa.String(length=200), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('package', 'donate_url') 24 | -------------------------------------------------------------------------------- /migrations/versions/c154912eaa0c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c154912eaa0c 4 | Revises: 7f166b5218d7 5 | Create Date: 2020-12-05 02:29:16.706564 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c154912eaa0c' 14 | down_revision = '7f166b5218d7' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.execute(text("COMMIT")) 21 | op.execute(text("ALTER TYPE auditseverity ADD VALUE 'USER'")) 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /migrations/versions/57b7fbc174cf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 57b7fbc174cf 4 | Revises: 1e08d7e4c15d 5 | Create Date: 2025-08-26 19:23:22.446424 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '57b7fbc174cf' 13 | down_revision = '1e08d7e4c15d' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.execute(text("COMMIT")) 20 | op.execute(text("ALTER TYPE reportcategory ADD VALUE 'REVIEW' AFTER 'ILLEGAL_HARMFUL'")) 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /app/templates/admin/versions/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Luanti Versions") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ _("New Version") }} 9 | 10 |

{{ _("Luanti Versions") }}

11 | 12 |
13 | {% for v in versions %} 14 | 16 | {{ v.name }} 17 | 18 | {% endfor %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /migrations/versions/23afcf580aae_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 23afcf580aae 4 | Revises: dabd7ab14339 5 | Create Date: 2023-05-11 22:02:24.021652 6 | 7 | """ 8 | 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '23afcf580aae' 13 | down_revision = 'dabd7ab14339' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.create_check_constraint("ck_license_txp", "package", "type != 'TXP' OR license_id = media_license_id") 20 | 21 | 22 | def downgrade(): 23 | op.drop_constraint("ck_license_txp", "package", type_="check") 24 | -------------------------------------------------------------------------------- /migrations/versions/a791b9b74a4c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: a791b9b74a4c 4 | Revises: 44e138485931 5 | Create Date: 2018-12-23 23:52:02.010281 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a791b9b74a4c' 14 | down_revision = '44e138485931' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('forum_topic', sa.Column('discarded', sa.Boolean(), server_default='0', nullable=True)) 21 | 22 | def downgrade(): 23 | op.drop_column('forum_topic', 'discarded') 24 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /app/templates/todo/modnames.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Unfulfilled Mod Names 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Unfulfilled Mod Names

9 | 10 |

11 | Mod names that have hard dependers, but are not fulfilled. 12 |

13 | 14 |
15 | {% for meta in modnames %} 16 | 18 | {{ meta.name }} 19 | 20 | {% else %} 21 |
  • No mod names found.
  • 22 | {% endfor %} 23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /migrations/versions/64fee8e5ab34_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 64fee8e5ab34 4 | Revises: 306ce331a2a7 5 | Create Date: 2020-01-19 02:28:05.432244 6 | 7 | """ 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = '64fee8e5ab34' 12 | down_revision = '306ce331a2a7' 13 | branch_labels = None 14 | depends_on = None 15 | 16 | 17 | def upgrade(): 18 | op.alter_column('user', 'confirmed_at', nullable=False, new_column_name='email_confirmed_at') 19 | 20 | 21 | def downgrade(): 22 | op.alter_column('user', 'email_confirmed_at', nullable=False, new_column_name='confirmed_at') 23 | -------------------------------------------------------------------------------- /migrations/versions/7a48dbd05780_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7a48dbd05780 4 | Revises: df66c78e6791 5 | Create Date: 2020-01-24 21:52:49.744404 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '7a48dbd05780' 13 | down_revision = 'df66c78e6791' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.add_column('user', sa.Column('github_access_token', sa.String(length=50), nullable=True, server_default=None)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('user', 'github_access_token') 24 | -------------------------------------------------------------------------------- /migrations/versions/8807a5279793_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8807a5279793 4 | Revises: 01f8d5de29e1 5 | Create Date: 2022-04-23 19:45:00.301875 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | revision = '8807a5279793' 13 | down_revision = '01f8d5de29e1' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.add_column('thread_reply', sa.Column('is_status_update', sa.Boolean(), server_default='0', nullable=False)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('thread_reply', 'is_status_update') 24 | -------------------------------------------------------------------------------- /migrations/versions/f94192c54b73_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f94192c54b73 4 | Revises: 97f9d84aae1e 5 | Create Date: 2025-11-08 13:34:18.185232 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'f94192c54b73' 15 | down_revision = '97f9d84aae1e' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.execute(text("ALTER TYPE releasestate ADD VALUE 'UNAPPROVED' AFTER 'APPROVED'")) 22 | 23 | 24 | def downgrade(): 25 | pass 26 | -------------------------------------------------------------------------------- /utils/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # The entrypoint for the docker containers 5 | # 6 | 7 | # Debug 8 | # FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_DEBUG=1 python3 -m flask run -h 0.0.0.0 -p 5123 9 | 10 | if [ -z "$FLASK_DEBUG" ]; then 11 | echo "FLASK_DEBUG is required in config.env" 12 | exit 1 13 | fi 14 | 15 | if [ "$FLASK_DEBUG" -eq "1" ]; then 16 | FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_RUN_PORT=5123 flask run --host=0.0.0.0 17 | else 18 | ENV="-e FLASK_APP=app/__init__.py -e FLASK_CONFIG=../config.cfg -e FLASK_DEBUG=$FLASK_DEBUG" 19 | gunicorn -w 4 -b :5123 $ENV app:app 20 | fi 21 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We only support the latest production version, deployed to . 6 | This is usually the latest `master` commit. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | We ask that you report vulnerabilities privately, by contacting rubenwardy, 11 | to give us time to fix them. You can do that by using one of the methods outlined in the following link: 12 | 13 | * https://rubenwardy.com/contact/ 14 | 15 | For more information on the justification of this policy, see 16 | [Responsible Disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure). 17 | -------------------------------------------------------------------------------- /app/templates/users/email_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Check Your Email") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 |

    11 | {{ _("We've sent an email to the address you specified.") }} 12 | {{ _("You'll need to click the link in the email to confirm it.") }}

    13 | 14 |

    15 | 16 | {{ _("The link will expire in 12 hours") }} 17 | 18 |

    19 | 20 |

    21 | 22 | {{ _("My email never arrived") }} 23 | 24 |

    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /app/templates/users/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Request Password Reset") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %} 9 | 10 |
    11 |

    {{ self.title() }}

    12 | 13 |
    14 | {{ form.hidden_tag() }} 15 | 16 | {{ render_field(form.email) }} 17 |

    18 | {{ render_submit_field(form.submit, tabindex=180) }} 19 |

    20 |
    21 |
    22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /migrations/versions/011e42c52d21_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 011e42c52d21 4 | Revises: 6e57b2b4dcdf 5 | Create Date: 2022-01-25 18:48:46.367409 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '011e42c52d21' 14 | down_revision = '6e57b2b4dcdf' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package', sa.Column('video_url', sa.String(length=200), nullable=True)) 21 | 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('package', 'video_url') -------------------------------------------------------------------------------- /migrations/versions/28a427cbd4cf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 28a427cbd4cf 4 | Revises: e9f534df23a8 5 | Create Date: 2018-06-03 01:47:33.006039 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '28a427cbd4cf' 11 | down_revision = 'e9f534df23a8' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | 16 | def upgrade(): 17 | # ### commands auto generated by Alembic - please adjust! ### 18 | pass 19 | # ### end Alembic commands ### 20 | 21 | 22 | def downgrade(): 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | pass 25 | # ### end Alembic commands ### 26 | -------------------------------------------------------------------------------- /app/templates/packages/user_screenshot_new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Add a screenshot") }} - {{ package.title }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ _("Add a screenshot") }}

    9 | 10 | {% from "macros/forms.html" import render_field, render_submit_field %} 11 |
    12 | {{ form.hidden_tag() }} 13 | 14 | {{ render_field(form.title) }} 15 | {{ render_field(form.file_upload, accept="image/png,image/jpeg,image/webp") }} 16 | {{ render_checkbox_field 17 | {{ render_submit_field(form.submit) }} 18 |
    19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /utils/restore_bk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Restores backup 4 | 5 | set -e 6 | . "${BASH_SOURCE%/*}/common.sh" 7 | 8 | if [ -z "$1" ]; then 9 | echo "Usage: ./utils/restore_bk.sh path/to/backup.sql" 10 | exit 1 11 | fi 12 | 13 | BKFILE=$1 14 | 15 | if [[ ! -f "$BKFILE" ]]; then 16 | echo "No such file: $BKFILE" 17 | exit 1 18 | fi 19 | 20 | if [[ "$BKFILE" == *.gpg ]]; then 21 | IN=$BKFILE 22 | BKFILE=/tmp/$(basename "$BKFILE" .gpg) 23 | echo "Decrypting backup at $IN to $BKFILE" 24 | gpg --decrypt "$IN" > "$BKFILE" 25 | fi 26 | 27 | echo "Importing backup from $BKFILE" 28 | cat $BKFILE | docker exec -i "$(container db)" psql contentdb contentdb 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Feature 6 | assignees: '' 7 | --- 8 | 9 | ## Problem 10 | 11 | A clear and concise description of what the problem is. 12 | ie: Why is this needed? 13 | Ex. I'm always frustrated when [...] 14 | 15 | ## Solutions 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | ## Alternatives 20 | 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | ## Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /migrations/versions/7f166b5218d7_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7f166b5218d7 4 | Revises: 3f5836a3df5c 5 | Create Date: 2020-12-05 00:06:41.466562 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7f166b5218d7' 14 | down_revision = '3f5836a3df5c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('user_email_verification', sa.Column('is_password_reset', sa.Boolean(), nullable=False, server_default="false")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('user_email_verification', 'is_password_reset') 25 | -------------------------------------------------------------------------------- /migrations/versions/1af840af0209_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1af840af0209 4 | Revises: 725ff70ea316 5 | Create Date: 2021-08-16 17:17:12.060257 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '1af840af0209' 15 | down_revision = '725ff70ea316' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.execute(text("COMMIT")) 22 | op.execute(text("ALTER TYPE userrank ADD VALUE 'APPROVER' BEFORE 'EDITOR'")) 23 | 24 | 25 | def downgrade(): 26 | pass 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Install docker compose 10 | run: sudo apt-get install -y docker-compose 11 | - uses: actions/checkout@v4 12 | - name: Copy config 13 | run: cp utils/ci/* . 14 | - name: Build the Docker image 15 | run: docker compose build 16 | - name: Start Docker 17 | run: docker compose up -d 18 | - name: Run migrations 19 | run: ./utils/run_migrations.sh 20 | - name: Run tests 21 | run: ./utils/tests_cov.sh 22 | - name: Stop Docker 23 | run: docker compose down 24 | -------------------------------------------------------------------------------- /app/templates/admin/audit_view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ entry.title }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% if entry.url %} 9 | View 10 | {% endif %} 11 | 12 |

    {{ entry.title }}

    13 | 14 | {% if entry.causer %} 15 |

    16 | {{ _("Caused by %(author)s.", author=entry.causer.username) }} 17 |

    18 | {% else %} 19 |

    20 | {{ _("Caused by a deleted user.") }} 21 |

    22 | {% endif %} 23 | 24 |
    {{ entry.description }}
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /app/templates/admin/send_bulk_notification.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Send bulk notification") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 |

    11 | BE VERY CAREFUL. 12 | This will send a notification to all active users. 13 |

    14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | {{ render_field(form.title) }} 19 | {{ render_field(form.url) }} 20 | {{ render_submit_field(form.submit) }} 21 |
    22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /migrations/versions/306ce331a2a7_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 306ce331a2a7 4 | Revises: 6dca6eceb04d 5 | Create Date: 2020-01-18 23:00:40.487425 6 | 7 | """ 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = '306ce331a2a7' 12 | down_revision = '6dca6eceb04d' 13 | branch_labels = None 14 | depends_on = None 15 | 16 | 17 | def upgrade(): 18 | conn = op.get_bind() 19 | op.create_check_constraint("CK_approval_valid", "package_release", "not approved OR (task_id IS NULL AND NOT url = '')") 20 | 21 | 22 | def downgrade(): 23 | conn = op.get_bind() 24 | op.drop_constraint("CK_approval_valid", "package_release", type_="check") 25 | -------------------------------------------------------------------------------- /migrations/versions/4585ce5147b8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 4585ce5147b8 4 | Revises: 105d4c740ad6 5 | Create Date: 2020-12-15 21:35:18.982716 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4585ce5147b8' 14 | down_revision = '105d4c740ad6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_update_config', sa.Column('outdated', sa.Boolean(), nullable=False, server_default="false")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('package_update_config', 'outdated') 25 | -------------------------------------------------------------------------------- /migrations/versions/e82c2141fae3_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e82c2141fae3 4 | Revises: 96811eb565c1 5 | Create Date: 2021-02-02 17:25:04.070483 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e82c2141fae3' 14 | down_revision = '96811eb565c1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_screenshot', sa.Column('created_at', sa.DateTime(), nullable=False, server_default="now()")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('package_screenshot', 'created_at') 25 | -------------------------------------------------------------------------------- /app/public/static/fa/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"} -------------------------------------------------------------------------------- /app/templates/admin/transfer.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Transfer Package(s) 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    Transfer Package(s)

    9 | 10 | {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %} 11 |
    12 | {{ form.hidden_tag() }} 13 | 14 | {{ render_field(form.old_username) }} 15 | {{ render_field(form.new_username) }} 16 | {{ render_field(form.package, hint="Leave blank to transfer all packages") }} 17 | {{ render_checkbox_field(form.remove_maintainer, class_="mb-5") }} 18 | {{ render_submit_field(form.submit) }} 19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/templates/packages/reviews_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Reviews") }} 5 | {% endblock %} 6 | 7 | {% block scriptextra %} 8 | {% if current_user.is_authenticated %} 9 | 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 | {% from "macros/pagination.html" import render_pagination with context %} 15 | {% from "macros/reviews.html" import render_reviews with context %} 16 | 17 | {{ render_pagination(pagination, url_set_query) }} 18 | {{ render_reviews(reviews, current_user, True) }} 19 | {{ render_pagination(pagination, url_set_query) }} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /migrations/versions/96811eb565c1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 96811eb565c1 4 | Revises: a337bcc165c0 5 | Create Date: 2021-01-29 23:14:37.806520 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '96811eb565c1' 14 | down_revision = 'a337bcc165c0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_update_config', sa.Column('auto_created', sa.Boolean(), nullable=False, server_default="false")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('package_update_config', 'auto_created') 25 | -------------------------------------------------------------------------------- /app/public/static/fa/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} -------------------------------------------------------------------------------- /migrations/versions/8425c06b7d77_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8425c06b7d77 4 | Revises: 8807a5279793 5 | Create Date: 2022-06-25 00:26:29.841145 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8425c06b7d77' 14 | down_revision = '8807a5279793' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package', sa.Column('enable_game_support_detection', sa.Boolean(), nullable=False, server_default="true")) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('package', 'enable_game_support_detection') 25 | -------------------------------------------------------------------------------- /migrations/versions/105d4c740ad6_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 105d4c740ad6 4 | Revises: 886c92dc6eaa 5 | Create Date: 2020-12-15 17:28:56.559801 6 | 7 | """ 8 | import datetime 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | from sqlalchemy import orm, text 16 | from app.models import User, UserRank 17 | 18 | revision = '105d4c740ad6' 19 | down_revision = '886c92dc6eaa' 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade(): 25 | op.execute(text("COMMIT")) 26 | op.execute(text("ALTER TYPE userrank ADD VALUE 'BOT' AFTER 'EDITOR'")) 27 | 28 | 29 | def downgrade(): 30 | pass 31 | -------------------------------------------------------------------------------- /app/public/static/fa/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400} -------------------------------------------------------------------------------- /migrations/versions/c141a63b2487_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c141a63b2487 4 | Revises: cb6ab141c522 5 | Create Date: 2020-07-09 00:05:39.845465 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'c141a63b2487' 13 | down_revision = 'cb6ab141c522' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.add_column('package', sa.Column('downloads', sa.Integer(), nullable=False, server_default="0")) 20 | 21 | 22 | def downgrade(): 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.drop_column('package', 'downloads') 25 | # ### end Alembic commands ### 26 | -------------------------------------------------------------------------------- /migrations/versions/e02ce7e98d92_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e02ce7e98d92 4 | Revises: f94192c54b73 5 | Create Date: 2025-11-28 20:16:32.330157 6 | """ 7 | 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | revision = 'e02ce7e98d92' 12 | down_revision = 'f94192c54b73' 13 | branch_labels = None 14 | depends_on = None 15 | 16 | 17 | def upgrade(): 18 | with op.batch_alter_table('package_update_config', schema=None) as batch_op: 19 | batch_op.add_column(sa.Column('task_id', sa.String(length=37), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | with op.batch_alter_table('package_update_config', schema=None) as batch_op: 24 | batch_op.drop_column('task_id') 25 | -------------------------------------------------------------------------------- /app/tests/integ/test_homepage.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from app.default_data import populate_test_data 6 | from app.models import db 7 | from .utils import client # noqa 8 | 9 | 10 | def test_homepage_empty(client): 11 | """Start with a blank database.""" 12 | 13 | rv = client.get("/") 14 | assert b"No packages available" in rv.data and b"packagegridscrub" not in rv.data 15 | 16 | 17 | def test_homepage_with_contents(client): 18 | """Start with a test database.""" 19 | 20 | populate_test_data(db.session) 21 | db.session.commit() 22 | 23 | rv = client.get("/") 24 | 25 | assert b"packagegridscrub" in rv.data 26 | -------------------------------------------------------------------------------- /migrations/versions/43dc7dbf64c8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 43dc7dbf64c8 4 | Revises: c1ea65e2b492 5 | Create Date: 2020-12-09 19:06:11.891807 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '43dc7dbf64c8' 14 | down_revision = 'c1ea65e2b492' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column('audit_log_entry', 'causer_id', 21 | existing_type=sa.INTEGER(), 22 | nullable=True) 23 | 24 | 25 | def downgrade(): 26 | op.alter_column('audit_log_entry', 'causer_id', 27 | existing_type=sa.INTEGER(), 28 | nullable=False) 29 | -------------------------------------------------------------------------------- /app/flatpages/help/feeds.md: -------------------------------------------------------------------------------- 1 | title: Feeds 2 | 3 | You can follow updates from ContentDB in your RSS feed reader. If in doubt, copy the Atom URL. 4 | 5 | * All events: [Atom]({{ url_for('feeds.all_atom') }}) | [JSONFeed]({{ url_for('feeds.all_json') }}) 6 | * New packages: [Atom]({{ url_for('feeds.packages_all_atom') }}) | [JSONFeed]({{ url_for('feeds.packages_all_json') }}) 7 | * New releases: [Atom]({{ url_for('feeds.releases_all_atom') }}) | [JSONFeed]({{ url_for('feeds.releases_all_json') }}) 8 | 9 | ## Package feeds 10 | 11 | Follow new releases for a package: 12 | 13 | ``` 14 | https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.atom 15 | https://content.luanti.org/packages/AUTHOR/NAME/releases_feed.json 16 | ``` 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.11-alpine 2 | 3 | RUN addgroup --gid 5123 cdb && \ 4 | adduser --uid 5123 -S cdb -G cdb 5 | 6 | WORKDIR /home/cdb 7 | 8 | RUN \ 9 | apk add --no-cache postgresql-libs git bash unzip && \ 10 | apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev g++ 11 | 12 | RUN mkdir /var/cdb 13 | RUN chown -R cdb:cdb /var/cdb 14 | 15 | COPY requirements.lock.txt requirements.lock.txt 16 | RUN pip install -r requirements.lock.txt && \ 17 | pip install gunicorn 18 | 19 | COPY utils utils 20 | COPY config.cfg config.cfg 21 | COPY migrations migrations 22 | COPY app app 23 | COPY translations translations 24 | 25 | RUN pybabel compile -d translations 26 | RUN chown -R cdb:cdb /home/cdb 27 | 28 | USER cdb 29 | -------------------------------------------------------------------------------- /migrations/versions/df66c78e6791_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: df66c78e6791 4 | Revises: a0f6c8743362 5 | Create Date: 2020-01-24 18:39:58.363417 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'df66c78e6791' 13 | down_revision = 'a0f6c8743362' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.add_column('api_token', sa.Column('package_id', sa.Integer(), nullable=True)) 20 | op.create_foreign_key(None, 'api_token', 'package', ['package_id'], ['id']) 21 | 22 | 23 | def downgrade(): 24 | op.drop_constraint(None, 'api_token', type_='foreignkey') 25 | op.drop_column('api_token', 'package_id') 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-FlatPages 3 | Flask-Login 4 | Flask-Migrate 5 | Flask-SQLAlchemy 6 | Flask-Babel 7 | Flask-Mail 8 | Flask-WTF 9 | GitHub-Flask 10 | SQLAlchemy-Searchable 11 | 12 | bcrypt 13 | markdown-it-py 14 | linkify-it-py 15 | mdit-py-plugins 16 | bleach 17 | passlib 18 | 19 | pygments 20 | 21 | beautifulsoup4 22 | celery 23 | kombu 24 | GitPython 25 | git-archive-all 26 | lxml 27 | pillow 28 | libsass 29 | redis 30 | psycopg2 31 | 32 | pytest 33 | pytest-cov 34 | 35 | email_validator 36 | 37 | pyyaml 38 | ua-parser 39 | user-agents 40 | 41 | Werkzeug 42 | SQLAlchemy 43 | WTForms 44 | WTForms-SQLAlchemy 45 | requests 46 | alembic 47 | 48 | validators 49 | gitdb 50 | 51 | deep-compare 52 | 53 | sentry-sdk[flask] 54 | -------------------------------------------------------------------------------- /migrations/versions/20f2aa2f40b9_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 20f2aa2f40b9 4 | Revises: 89dfa0043f9c 5 | Create Date: 2023-08-19 01:35:20.100549 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '20f2aa2f40b9' 14 | down_revision = '89dfa0043f9c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('collection', sa.Column('long_description', sa.UnicodeText(), nullable=True, server_default=None)) 21 | 22 | 23 | def downgrade(): 24 | with op.batch_alter_table('collection', schema=None) as batch_op: 25 | batch_op.drop_column('long_description') 26 | -------------------------------------------------------------------------------- /app/templates/threads/edit_reply.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Edit reply") }} - {{ thread.title }} 5 | {% endblock %} 6 | 7 | {% block scriptextra %} 8 | {% from "macros/forms.html" import easymde_scripts %} 9 | {{ easymde_scripts() }} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |

    {{ _("Edit reply") }}

    14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.comment, label="", class_="m-0", fieldclass="form-control markdown", data_enter_submit="1") }}
    20 | {{ render_submit_field(form.btn_submit) }} 21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /app/templates/admin/licenses/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Licenses 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ _("New License") }} 9 | 10 |

    {{ _("Licenses") }}

    11 | 12 |
    13 | {% for l in licenses %} 14 | 16 | 17 | {{ l.is_foss and "Free" or "Non-free"}} 18 | 19 | {{ l.name }} 20 | 21 | {% endfor %} 22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/templates/collections/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _('Delete collection "%(title)s" by %(author)s', title=collection.title, author=collection.author.username) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 | 10 |

    {{ self.title() }}

    11 |
    12 |

    {{ _("Deleting is permanent") }}

    13 | {{ _("Cancel") }} 14 | 15 |
    16 |
    17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/templates/admin/audit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Audit Log 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    Audit Log

    9 | 10 | {% from "macros/forms.html" import render_field, render_submit_field %} 11 |
    12 | {{ render_field(form.username) }} 13 | {{ render_field(form.q) }} 14 | {{ render_field(form.url) }} 15 | {{ render_submit_field(form.submit) }} 16 |
    17 | 18 | {% from "macros/pagination.html" import render_pagination %} 19 | {% from "macros/audit_log.html" import render_audit_log %} 20 | 21 | {{ render_pagination(pagination, url_set_query) }} 22 | {{ render_audit_log(log, current_user) }} 23 | {{ render_pagination(pagination, url_set_query) }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /migrations/versions/2ecff2f9972d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2ecff2f9972d 4 | Revises: 23afcf580aae 5 | Create Date: 2023-06-18 07:51:42.581955 6 | 7 | """ 8 | 9 | from alembic import op 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2ecff2f9972d' 14 | down_revision = '23afcf580aae' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('package', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('supports_all_games', sa.Boolean(), nullable=False, server_default="false")) 22 | 23 | 24 | def downgrade(): 25 | with op.batch_alter_table('package', schema=None) as batch_op: 26 | batch_op.drop_column('supports_all_games') 27 | -------------------------------------------------------------------------------- /migrations/versions/9395ba96f853_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9395ba96f853 4 | Revises: dd17239f7144 5 | Create Date: 2023-10-31 17:39:27.957209 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9395ba96f853' 14 | down_revision = 'dd17239f7144' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('oauth_client', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=False)) 22 | 23 | def downgrade(): 24 | with op.batch_alter_table('oauth_client', schema=None) as batch_op: 25 | batch_op.drop_column('created_at') 26 | -------------------------------------------------------------------------------- /migrations/versions/e1bf78a597a2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e1bf78a597a2 4 | Revises: 06d23947e7ef 5 | Create Date: 2020-12-06 03:16:59.988464 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e1bf78a597a2' 14 | down_revision = '06d23947e7ef' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_screenshot', sa.Column('order', sa.Integer(), nullable=True)) 21 | op.execute(text("""UPDATE package_screenshot SET "order" = id""")) 22 | op.alter_column('package_screenshot', 'order', nullable=False) 23 | 24 | 25 | def downgrade(): 26 | op.drop_column('package_screenshot', 'order') 27 | -------------------------------------------------------------------------------- /app/templates/report/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title -%} 4 | Edit report 5 | {%- endblock %} 6 | 7 | {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field, easymde_scripts %} 8 | {% block scriptextra %} 9 | {{ easymde_scripts() }} 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |

    {{ self.title() }}

    15 | 16 |
    17 | {{ form.hidden_tag() }} 18 | {{ render_field(form.category) }} 19 | {{ render_field(form.url) }} 20 | {{ render_field(form.title) }} 21 | {{ render_field(form.message, class_="m-0", fieldclass="form-control markdown", data_enter_submit="1") }} 22 | {{ render_submit_field(form.submit) }} 23 |
    24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /app/scss/comments.scss: -------------------------------------------------------------------------------- 1 | .img-thumbnail-1 { 2 | padding: 0px; 3 | // width: 100%; 4 | border: unset; 5 | } 6 | 7 | .comments { 8 | list-style: none; 9 | padding: 0; 10 | 11 | .card { 12 | position:relative; 13 | 14 | .card-header:before { 15 | position: absolute; 16 | top: 11px; 17 | right: 100%; 18 | width: 0; 19 | height: 0; 20 | display: block; 21 | content:" "; 22 | border-color: transparent; 23 | border-style: solid solid outset; 24 | pointer-events:none; 25 | border-right-color: #444; 26 | border-width: 14px; 27 | } 28 | } 29 | 30 | .user-photo { 31 | display: inline-block; 32 | width: 60px; 33 | height: 60px; 34 | object-fit: cover; 35 | aspect-ratio: 1; 36 | } 37 | 38 | .status-update p { 39 | margin: 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/templates/packages/alias_create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Alias") }} 5 | {% endblock %} 6 | 7 | {% block link %} 8 | {{ package.title }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 | {{ _("Back to Aliases") }} 14 | 15 | 16 | {% from "macros/forms.html" import render_field, render_submit_field, render_toggle_field %} 17 |
    18 | {{ form.hidden_tag() }} 19 | 20 | {{ render_field(form.author) }} 21 | {{ render_field(form.name) }} 22 | {{ render_submit_field(form.submit) }} 23 |
    24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /migrations/versions/ea5a023711e0_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ea5a023711e0 4 | Revises: fa12fadbdb40 5 | Create Date: 2018-05-26 01:55:09.745881 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'ea5a023711e0' 13 | down_revision = 'fa12fadbdb40' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | conn = op.get_bind() 21 | conn.execute(text("ALTER TYPE userrank ADD VALUE 'BANNED'")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | pass 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/public/static/fa/css/brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: normal; 9 | font-display: auto; 10 | src: url("../webfonts/fa-brands-400.eot"); 11 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 12 | 13 | .fab { 14 | font-family: 'Font Awesome 5 Brands'; } 15 | -------------------------------------------------------------------------------- /app/templates/admin/versions/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if version %} 5 | Edit {{ version.name }} 6 | {% else %} 7 | New Luanti Version 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | New Version 13 | Back to list 14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.name) }} 20 | {{ render_field(form.protocol) }} 21 | {{ render_submit_field(form.submit) }} 22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/templates/emails/verify_unsubscribe.html: -------------------------------------------------------------------------------- 1 | {% extends "emails/base.html" %} 2 | 3 | {% block content %} 4 |

    5 | {{ _("Hello!") }} 6 |

    7 | 8 |

    9 | {{ _("We're sorry to see you go. You just need to do one more thing before your email is blacklisted.") }} 10 |

    11 | 12 | 13 | {{ _("Unsubscribe") }} 14 | 15 | 16 |

    17 | {{ _("Or paste this into your browser:") }} {{ abs_url_for('users.unsubscribe', token=sub.token) }} 18 |

    19 | 20 | {% endblock %} 21 | 22 | {% block footer %} 23 | {{ _("You are receiving this email because someone (hopefully you) entered your email address in the unsubscribe form.") }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /migrations/versions/06af23184d15_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 06af23184d15 4 | Revises: e02ce7e98d92 5 | Create Date: 2025-12-15 18:40:13.829344 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '06af23184d15' 14 | down_revision = 'e02ce7e98d92' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('notification', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('read_at', sa.DateTime(), nullable=True, default=None)) 22 | 23 | 24 | def downgrade(): 25 | with op.batch_alter_table('notification', schema=None) as batch_op: 26 | batch_op.drop_column('read_at') 27 | -------------------------------------------------------------------------------- /migrations/versions/49105d276908_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 49105d276908 4 | Revises: 7a749a6c8c3a 5 | Create Date: 2023-10-01 23:25:24.870407 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '49105d276908' 14 | down_revision = '7a749a6c8c3a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('package', schema=None) as batch_op: 21 | batch_op.create_unique_constraint('_package_uc', ['author_id', 'name']) 22 | 23 | 24 | def downgrade(): 25 | with op.batch_alter_table('package', schema=None) as batch_op: 26 | batch_op.drop_constraint('_package_uc', type_='unique') 27 | -------------------------------------------------------------------------------- /migrations/versions/b254f55eadd2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b254f55eadd2 4 | Revises: 4e482c47e519 5 | Create Date: 2018-05-27 23:51:11.008936 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'b254f55eadd2' 13 | down_revision = '4e482c47e519' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | conn = op.get_bind() 21 | conn.execute(text("ALTER TYPE userrank ADD VALUE 'TRUSTED_MEMBER'")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | pass 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/templates/packages/screenshot_new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Add a screenshot") }} - {{ package.title }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ _("Add a screenshot") }}

    9 |

    10 | {{ _("The recommended resolution is 1920x1080, and screenshots must be at least %(width)dx%(height)d.", 11 | width=920, height=517) }} 12 |

    13 | 14 | {% from "macros/forms.html" import render_field, render_submit_field %} 15 |
    16 | {{ form.hidden_tag() }} 17 | 18 | {{ render_field(form.title) }} 19 | {{ render_field(form.file_upload, accept="image/png,image/jpeg,image/webp") }} 20 | {{ render_submit_field(form.submit) }} 21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /app/public/static/fa/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: auto; 10 | src: url("../webfonts/fa-solid-900.eot"); 11 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 12 | 13 | .fa, 14 | .fas { 15 | font-family: 'Font Awesome 5 Free'; 16 | font-weight: 900; } 17 | -------------------------------------------------------------------------------- /app/templates/admin/languages/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if language %} 5 | Edit {{ language.title }} 6 | {% else %} 7 | New language 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | New Language 13 | Back to list 14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.id) }} 20 | {{ render_field(form.title) }} 21 | {{ render_submit_field(form.submit) }} 22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/templates/packages/share.html: -------------------------------------------------------------------------------- 1 | {% extends "packages/package_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Share and Badges") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 |

    {{ _("Links") }}

    11 | 12 |

    13 | {{ _("Review link") }}: 14 |

    15 | 16 |
    {{ package.get_url("packages.review", absolute=True) }}
    17 | 18 |

    {{ _("Badges") }}

    19 | 20 |

    21 | {{ package.make_shield("title") | markdown }} 22 |

    23 | 24 |

    25 |

    {{ package.make_shield("title") }}
    26 |

    27 | 28 |

    29 | {{ package.make_shield("downloads") | markdown }} 30 |

    31 | 32 |

    33 |

    {{ package.make_shield("downloads") }}
    34 |

    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /migrations/versions/1acc6e90bbac_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1acc6e90bbac 4 | Revises: 57b7fbc174cf 5 | Create Date: 2025-08-26 20:23:29.086541 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1acc6e90bbac' 14 | down_revision = '57b7fbc174cf' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('package_update_config', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('last_checked_at', sa.DateTime(), nullable=True)) 22 | 23 | 24 | def downgrade(): 25 | with op.batch_alter_table('package_update_config', schema=None) as batch_op: 26 | batch_op.drop_column('last_checked_at') 27 | -------------------------------------------------------------------------------- /migrations/versions/6e59ad5cc62a_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6e59ad5cc62a 4 | Revises: 8425c06b7d77 5 | Create Date: 2022-06-25 02:39:15.959553 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6e59ad5cc62a' 14 | down_revision = '8425c06b7d77' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_constraint("name_valid", "package", type_="check") 21 | op.create_check_constraint("name_valid", "package", "name ~* '^[a-z0-9_]+$' AND name != '_game'") 22 | 23 | 24 | def downgrade(): 25 | op.drop_constraint("name_valid", "package", type_="check") 26 | op.create_check_constraint("name_valid", "package", "name ~* '^[a-z0-9_]+$'") 27 | -------------------------------------------------------------------------------- /app/public/static/fa/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: auto; 10 | src: url("../webfonts/fa-regular-400.eot"); 11 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 12 | 13 | .far { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /migrations/versions/dce69ad1e4eb_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: dce69ad1e4eb 4 | Revises: a791b9b74a4c 5 | Create Date: 2018-12-25 18:57:44.575501 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dce69ad1e4eb' 14 | down_revision = 'a791b9b74a4c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column("user", sa.Column('profile_pic', sa.String(length=255), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column("user", "profile_pic") 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/templates/users/stats.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Statistics for %(display_name)s's packages", display_name=user.display_name) }} 5 | {% endblock %} 6 | 7 | {% from "macros/stats.html" import render_package_stats, render_package_stats_js, 8 | render_package_selector, render_daterange_selector with context %} 9 | 10 | {% block scriptextra %} 11 | {{ render_package_stats_js() }} 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
    16 | {{ render_daterange_selector(options, start or end) }} 17 | {{ render_package_selector(user, package=None) }} 18 |
    19 |

    {{ self.title() }}

    20 | {{ render_package_stats(url_for("api.user_stats", username=user.username, start=start, end=end), downloads, start or end) }} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /migrations/versions/11b6ef362f98_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 11b6ef362f98 4 | Revises: 9fc23495713b 5 | Create Date: 2018-07-04 01:01:45.440662 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '11b6ef362f98' 14 | down_revision = '9fc23495713b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('score', sa.Float(), nullable=False, server_default="0.0")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('package', 'score') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/7a749a6c8c3a_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7a749a6c8c3a 4 | Revises: 20f2aa2f40b9 5 | Create Date: 2023-08-20 21:19:26.930069 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7a749a6c8c3a' 14 | down_revision = '20f2aa2f40b9' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('tag', schema=None) as batch_op: 21 | batch_op.drop_column('is_protected') 22 | 23 | 24 | def downgrade(): 25 | with op.batch_alter_table('tag', schema=None) as batch_op: 26 | batch_op.add_column(sa.Column('is_protected', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False)) 27 | -------------------------------------------------------------------------------- /migrations/versions/86512692b770_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 86512692b770 4 | Revises: ba730ce1dc3e 5 | Create Date: 2020-07-11 01:56:28.634661 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '86512692b770' 14 | down_revision = 'ba730ce1dc3e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('audit_log_entry', sa.Column('description', sa.Text, nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('audit_log_entry', 'description') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/8679442b8dde_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8679442b8dde 4 | Revises: f612e293070a 5 | Create Date: 2020-07-11 00:14:02.330903 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8679442b8dde' 14 | down_revision = 'f612e293070a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('thread', sa.Column('locked', sa.Boolean(), server_default='0', nullable=False)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('thread', 'locked') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/c181c6c88bae_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c181c6c88bae 4 | Revises: daa040b727b2 5 | Create Date: 2025-07-02 17:21:33.554960 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c181c6c88bae' 14 | down_revision = 'daa040b727b2' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_release', 21 | sa.Column('file_size_bytes', sa.Integer(), nullable=False, server_default="0")) 22 | op.add_column('package_screenshot', 23 | sa.Column('file_size_bytes', sa.Integer(), nullable=False, server_default="0")) 24 | 25 | 26 | def downgrade(): 27 | op.drop_column('package', 'file_size_bytes') 28 | op.drop_column('package_screenshot', 'file_size_bytes') 29 | -------------------------------------------------------------------------------- /app/rediscache.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from . import redis_client 6 | 7 | # This file acts as a facade between the rest of the code and redis, 8 | # and also means that the rest of the code avoids knowing about `app` 9 | 10 | 11 | EXPIRY_TIME_S = 2*7*24*60*60 # 2 weeks 12 | 13 | 14 | def make_download_key(ip, package): 15 | return "{}/{}/{}".format(ip, package.author.username, package.name) 16 | 17 | 18 | def set_temp_key(key, v): 19 | redis_client.set(key, v, ex=EXPIRY_TIME_S) 20 | 21 | 22 | def has_key(key): 23 | return redis_client.exists(key) 24 | 25 | 26 | def increment_key(key): 27 | redis_client.incrby(key, 1) 28 | 29 | 30 | def get_key(key, default=None): 31 | return redis_client.get(key) or default 32 | -------------------------------------------------------------------------------- /app/templates/threads/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Threads") }} 5 | {% if package %} 6 | - {{ package.title }} 7 | {% endif %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if current_user.is_authenticated and package %} 12 | {{ _("New Thread") }} 13 | {% endif %} 14 |

    {{ self.title() }}

    15 | 16 | {% from "macros/pagination.html" import render_pagination %} 17 | {% from "macros/threads.html" import render_threadlist %} 18 | {{ render_pagination(pagination, url_set_query) }} 19 | 20 |
    21 | {{ render_threadlist(threads) }} 22 |
    23 | 24 | {{ render_pagination(pagination, url_set_query) }} 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /migrations/versions/9e2ac631efb0_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9e2ac631efb0 4 | Revises: 11b6ef362f98 5 | Create Date: 2018-07-06 23:16:50.507010 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9e2ac631efb0' 14 | down_revision = '11b6ef362f98' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('forum_topic', sa.Column('wip', sa.Boolean(), nullable=False, server_default="0")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('forum_topic', 'wip') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/d0bec9e5698e_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: d0bec9e5698e 4 | Revises: aa6d7b595a94 5 | Create Date: 2018-05-29 21:23:43.847738 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd0bec9e5698e' 14 | down_revision = 'aa6d7b595a94' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('license', sa.Column('is_foss', sa.Boolean(), nullable=False, server_default="true")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('license', 'is_foss') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/3a24fc02365e_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3a24fc02365e 4 | Revises: b370c3eb4227 5 | Create Date: 2020-07-17 20:58:31.130449 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3a24fc02365e' 14 | down_revision = 'b370c3eb4227' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('tag', sa.Column('description', sa.String(length=500), nullable=True, server_default=None)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('tag', 'description') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/a0f6c8743362_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: a0f6c8743362 4 | Revises: 64fee8e5ab34 5 | Create Date: 2020-01-19 19:12:39.402679 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'a0f6c8743362' 13 | down_revision = '64fee8e5ab34' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.alter_column('user', 'password', 20 | existing_type=sa.VARCHAR(length=255), 21 | nullable=False, 22 | existing_server_default=sa.text("''::character varying"), 23 | server_default='') 24 | 25 | 26 | def downgrade(): 27 | op.alter_column('user', 'password', 28 | existing_type=sa.VARCHAR(length=255), 29 | nullable=True, 30 | existing_server_default=sa.text("''::character varying")) 31 | -------------------------------------------------------------------------------- /app/templates/threads/delete_reply.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Delete reply by %(username)s in %(title)s ", title=thread.title, username=reply.author.username) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 | 10 | 11 |

    {{ self.title() }}

    12 |
    13 | {{ reply.comment | markdown }} 14 |
    15 |
    16 |

    {{ _("Deleting is permanent") }}

    17 | 18 | {{ _("Cancel") }} 19 | 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /app/templates/threads/delete_thread.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _('Delete "%(title)s" by %(author)s', title=thread.title, author=thread.author.username) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 | 10 | 11 |

    {{ self.title() }}

    12 |
    13 | {{ thread.first_reply.comment | markdown }} 14 |
    15 |
    16 |

    {{ _("Deleting is permanent") }}

    17 | 18 | {{ _("Cancel") }} 19 | 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /migrations/versions/97a9c461bc2d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 97a9c461bc2d 4 | Revises: 7def3e843d04 5 | Create Date: 2019-01-28 20:49:41.831991 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '97a9c461bc2d' 13 | down_revision = '7def3e843d04' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('minetest_release', sa.Column('protocol', sa.Integer(), nullable=False, server_default="0")) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('minetest_release', 'protocol') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/9ec17b558413_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9ec17b558413 4 | Revises: 97a9c461bc2d 5 | Create Date: 2019-01-29 00:37:49.507631 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '9ec17b558413' 13 | down_revision = '97a9c461bc2d' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('package_release', sa.Column('downloads', sa.Integer(), nullable=False, server_default="0")) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('package_release', 'downloads') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/c4152f4240ed_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c4152f4240ed 4 | Revises: 3f4d7cd8401f 5 | Create Date: 2018-05-25 18:27:16.953305 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c4152f4240ed' 14 | down_revision = '3f4d7cd8401f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('soft_deleted', sa.Boolean(), nullable=False, server_default="false")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('package', 'soft_deleted') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/f6ef5f35abca_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f6ef5f35abca 4 | Revises: 011e42c52d21 5 | Create Date: 2022-01-26 00:10:46.610784 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f6ef5f35abca' 14 | down_revision = '011e42c52d21' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_screenshot', sa.Column('height', sa.Integer(), nullable=False, server_default="0")) 21 | op.add_column('package_screenshot', sa.Column('width', sa.Integer(), nullable=False, server_default="0")) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('package_screenshot', 'width') 26 | op.drop_column('package_screenshot', 'height') 27 | -------------------------------------------------------------------------------- /app/templates/admin/licenses/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if license %} 5 | Edit {{ license.name }} 6 | {% else %} 7 | New license 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | New License 13 | Back to list 14 | 15 | {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.name) }} 20 | {{ render_checkbox_field(form.is_foss) }} 21 | {{ render_field(form.url) }} 22 | {{ render_submit_field(form.submit) }} 23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /migrations/versions/76ff303f76d8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 76ff303f76d8 4 | Revises: 6e59ad5cc62a 5 | Create Date: 2022-08-18 15:41:28.411877 6 | 7 | """ 8 | 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | from sqlalchemy_searchable import sync_trigger 13 | 14 | revision = '76ff303f76d8' 15 | down_revision = '6e59ad5cc62a' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | conn = op.get_bind() 22 | 23 | options = {"weights": {"name": "A", "title": "B", "short_desc": "C", "desc": "D"}} 24 | # sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"], options=options) 25 | 26 | 27 | def downgrade(): 28 | conn = op.get_bind() 29 | # sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"]) 30 | -------------------------------------------------------------------------------- /migrations/versions/fa12fadbdb40_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: fa12fadbdb40 4 | Revises: c4152f4240ed 5 | Create Date: 2018-05-25 18:46:54.039870 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'fa12fadbdb40' 14 | down_revision = 'c4152f4240ed' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package_screenshot', sa.Column('approved', sa.Boolean(), nullable=False, server_default="true")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('package_screenshot', 'approved') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/flatpages/help/metrics.md: -------------------------------------------------------------------------------- 1 | title: Prometheus Metrics 2 | 3 | ## What is Prometheus? 4 | 5 | [Prometheus](https://prometheus.io) is an "open-source monitoring system with a 6 | dimensional data model, flexible query language, efficient time series database 7 | and modern alerting approach". 8 | 9 | Prometheus Metrics can be accessed at [/metrics](/metrics), or you can view them 10 | on the Grafana instance below. 11 | 12 | {% if monitoring_url %} 13 |

    14 | 15 | View ContentDB on Grafana 16 | 17 |

    18 | {% endif %} 19 | 20 | ## Metrics 21 | 22 | * `contentdb_packages` - Total packages (counter). 23 | * `contentdb_users` - Number of registered users (counter). 24 | * `contentdb_downloads` - Total downloads (counter). 25 | * `contentdb_score` - Total package score (gauge). 26 | -------------------------------------------------------------------------------- /migrations/versions/13113e5710da_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 13113e5710da 4 | Revises: ead35f7d446c 5 | Create Date: 2018-05-23 20:18:07.606646 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '13113e5710da' 14 | down_revision = 'ead35f7d446c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp())) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('package', 'created_at') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/44e138485931_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 44e138485931 4 | Revises: 9e2ac631efb0 5 | Create Date: 2018-07-28 14:45:28.879331 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '44e138485931' 14 | down_revision = '9e2ac631efb0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package_release', sa.Column('commit_hash', sa.String(length=41), nullable=True, server_default=None)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('package_release', 'commit_hash') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/6dca6eceb04d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6dca6eceb04d 4 | Revises: fd25bf3e57c3 5 | Create Date: 2020-01-18 17:32:21.885068 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy_searchable import sync_trigger 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6dca6eceb04d' 14 | down_revision = 'fd25bf3e57c3' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | conn = op.get_bind() 21 | # sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"]) 22 | op.create_check_constraint("name_valid", "package", "name ~* '^[a-z0-9_]+$'") 23 | 24 | 25 | def downgrade(): 26 | conn = op.get_bind() 27 | # sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"]) 28 | op.drop_constraint("name_valid", "package", type_="check") 29 | -------------------------------------------------------------------------------- /migrations/versions/725ff70ea316_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 725ff70ea316 4 | Revises: 51be0401bb85 5 | Create Date: 2021-07-31 19:10:36.683434 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '725ff70ea316' 14 | down_revision = '51be0401bb85' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('license', sa.Column('url', sa.String(length=128), nullable=True, default=None)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('license', 'url') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/blueprints/admin/update_config.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from flask import render_template 6 | 7 | from . import bp 8 | from app.models import db, PackageUpdateConfig, Package, UserRank, PackageState 9 | from app.utils import rank_required 10 | 11 | 12 | @bp.route("/admin/update_config/") 13 | @rank_required(UserRank.APPROVER) 14 | def update_config(): 15 | failing_packages = (db.session.query(Package) 16 | .select_from(PackageUpdateConfig) 17 | .filter(PackageUpdateConfig.task_id != None) 18 | .order_by(PackageUpdateConfig.last_checked_at.desc()) 19 | .join(PackageUpdateConfig.package) 20 | .filter(Package.state == PackageState.APPROVED) 21 | .all()) 22 | return render_template("admin/update_config.html", failing_packages=failing_packages) 23 | -------------------------------------------------------------------------------- /app/public/static/js/release_new.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | window.addEventListener("load", () => { 7 | function check_opt() { 8 | if (document.querySelector("input[name='upload_mode']:checked").value === "vcs") { 9 | document.getElementById("file_upload").parentElement.classList.add("d-none"); 10 | document.getElementById("vcs_label").parentElement.classList.remove("d-none"); 11 | } else { 12 | document.getElementById("file_upload").parentElement.classList.remove("d-none"); 13 | document.getElementById("vcs_label").parentElement.classList.add("d-none"); 14 | } 15 | } 16 | 17 | document.querySelectorAll("input[name='upload_mode']").forEach(x => x.addEventListener("change", check_opt)); 18 | check_opt(); 19 | }); 20 | -------------------------------------------------------------------------------- /app/templates/admin/warnings/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if warning %} 5 | Edit {{ warning.title }} 6 | {% else %} 7 | New warning 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | New Warning 13 | Back to list 14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.title) }} 20 | {{ render_field(form.description) }} 21 | {% if warning %} 22 | {{ render_field(form.name) }} 23 | {% endif %} 24 | {{ render_submit_field(form.submit) }} 25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /migrations/versions/d4262fb15b37_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: d4262fb15b37 4 | Revises: 8ee3cf3fb312 5 | Create Date: 2021-07-22 10:59:03.217264 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd4262fb15b37' 14 | down_revision = '8ee3cf3fb312' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('tag', sa.Column('is_protected', sa.Boolean(), nullable=False, server_default="false")) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('tag', 'is_protected') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /utils/ci/config.cfg: -------------------------------------------------------------------------------- 1 | USER_APP_NAME="ContentDB" 2 | SERVER_NAME="localhost:5123" 3 | BASE_URL="http://" + SERVER_NAME 4 | 5 | SECRET_KEY="changeme" 6 | WTF_CSRF_SECRET_KEY="changeme" 7 | 8 | SQLALCHEMY_DATABASE_URI = "postgresql://contentdb:password@db:5432/contentdb" 9 | 10 | GITHUB_CLIENT_ID = "" 11 | GITHUB_CLIENT_SECRET = "" 12 | 13 | REDIS_URL='redis://redis:6379' 14 | CELERY_BROKER_URL='redis://redis:6379' 15 | CELERY_RESULT_BACKEND='redis://redis:6379' 16 | 17 | USER_ENABLE_USERNAME = True 18 | USER_ENABLE_REGISTER = False 19 | USER_ENABLE_CHANGE_USERNAME = False 20 | USER_ENABLE_EMAIL = False 21 | 22 | MAIL_UTILS_ERROR_SEND_TO = [""] 23 | 24 | UPLOAD_DIR="/var/cdb/uploads/" 25 | THUMBNAIL_DIR="/var/cdb/thumbnails/" 26 | 27 | TEMPLATES_AUTO_RELOAD = True 28 | 29 | LANGUAGES = { 30 | 'en': 'English', 31 | } 32 | 33 | ADMIN_CONTACT_URL = "https://example.org" 34 | -------------------------------------------------------------------------------- /app/templates/packages/edit_maintainers.html: -------------------------------------------------------------------------------- 1 | {% extends "packages/package_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Edit Maintainers") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ _("Maintainers") }}

    9 | {% from "macros/forms.html" import render_submit_field, render_field %} 10 |

    11 | {{ _("Maintainers are given write access to the package.") }} 12 | {{ _("Depending on their rank, they will be able to edit the package, create releases and screenshots, and read private threads.") }} 13 | {{ _("Maintainers cannot add or remove other maintainers, but can remove themselves.") }} 14 |

    15 | 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.maintainers_str) }} 20 | 21 |
    {{ render_submit_field(form.submit) }}
    22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /migrations/versions/3f4d7cd8401f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3f4d7cd8401f 4 | Revises: 13113e5710da 5 | Create Date: 2018-05-25 17:53:13.215127 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '3f4d7cd8401f' 13 | down_revision = '13113e5710da' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | conn = op.get_bind() 21 | conn.execute(text("ALTER TYPE packagepropertykey ADD VALUE 'harddeps'")) 22 | conn.execute(text("ALTER TYPE packagepropertykey ADD VALUE 'softdeps'")) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | pass 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/019da77ba02d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 019da77ba02d 4 | Revises: 4f2e19bc2a27 5 | Create Date: 2020-07-09 04:07:23.926213 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import datetime 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '019da77ba02d' 15 | down_revision = '4f2e19bc2a27' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('package_review', sa.Column('created_at', sa.DateTime(), nullable=False, server_default=datetime.datetime.utcnow().isoformat())) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('package_review', 'created_at') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /app/templates/users/claim.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Create Account") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 |

    {{ _("Do you have an account on the Luanti Forums?") }}

    11 | 12 |

    13 | {{ _("ContentDB will link your account to your forum account if you have one, but you don't need one.") }} 14 |

    15 | 16 |

    17 | 18 | {{ _("Yes, I have a forums account") }} 19 | 20 | 21 | {{ _("No, I don't have one") }} 22 | 23 | 24 | {{ _("Create forum account") }} 25 | 26 |

    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /migrations/versions/96a01fe23389_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 96a01fe23389 4 | Revises: cd5ab8a01f4a 5 | Create Date: 2021-11-24 17:12:33.893988 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '96a01fe23389' 15 | down_revision = 'cd5ab8a01f4a' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.execute(text("DELETE FROM user_email_verification")) 22 | op.add_column('user', sa.Column('created_at', sa.DateTime(), nullable=True)) 23 | op.add_column('user_email_verification', sa.Column('created_at', sa.DateTime(), nullable=False)) 24 | 25 | 26 | def downgrade(): 27 | 28 | op.drop_column('user_email_verification', 'created_at') 29 | op.drop_column('user', 'created_at') 30 | -------------------------------------------------------------------------------- /app/templates/packages/alias_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Aliases") }} 5 | {% endblock %} 6 | 7 | {% block link %} 8 | {{ package.title }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 | {{ _("Create") }} 14 | 15 |

    {{ _("Aliases for %(title)s by %(author)s", title=self.link(), author=package.author.display_name) }}

    16 | 17 |
    18 | {% for alias in package.aliases %} 19 | 20 | {{ alias.author }} / {{ alias.name }} 21 | 22 | {% else %} 23 |
    24 | {{ _("No aliases") }} 25 |
    26 | {% endfor %} 27 |
    28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /app/tests/unit/utils/test_url.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from app.utils.url import clean_youtube_url 6 | 7 | 8 | def test_clean_youtube_url(): 9 | assert clean_youtube_url( 10 | "https://www.youtube.com/watch?v=AABBCC") == "https://www.youtube.com/watch?v=AABBCC" 11 | assert clean_youtube_url( 12 | "https://www.youtube.com/watch?v=boGcB4H5-WA&other=1") == "https://www.youtube.com/watch?v=boGcB4H5-WA" 13 | assert clean_youtube_url("https://www.youtube.com/watch?kk=boGcB4H5-WA&other=1") is None 14 | assert clean_youtube_url("https://www.bob.com/watch?v=AABBCC") is None 15 | 16 | assert clean_youtube_url("https://youtu.be/boGcB4H5-WA") == "https://www.youtube.com/watch?v=boGcB4H5-WA" 17 | assert clean_youtube_url("https://youtu.be/boGcB4H5-WA?this=1") == "https://www.youtube.com/watch?v=boGcB4H5-WA" 18 | -------------------------------------------------------------------------------- /migrations/versions/dd17239f7144_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: dd17239f7144 4 | Revises: f0622f7671d5 5 | Create Date: 2023-10-31 16:29:08.892647 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dd17239f7144' 14 | down_revision = 'f0622f7671d5' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('api_token', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('auth_code', sa.String(length=34), nullable=True)) 22 | batch_op.create_unique_constraint(None, ['auth_code']) 23 | 24 | 25 | def downgrade(): 26 | with op.batch_alter_table('api_token', schema=None) as batch_op: 27 | batch_op.drop_constraint(None, type_='unique') 28 | batch_op.drop_column('auth_code') 29 | -------------------------------------------------------------------------------- /migrations/versions/ead35f7d446c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ead35f7d446c 4 | Revises: 81e0eb07a3cd 5 | Create Date: 2018-05-23 19:39:29.216273 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ead35f7d446c' 14 | down_revision = '81e0eb07a3cd' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('package', 'forums', 22 | existing_type=sa.INTEGER(), 23 | nullable=True) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.alter_column('package', 'forums', 30 | existing_type=sa.INTEGER(), 31 | nullable=False) 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /app/public/static/js/release_bulk_change.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | window.addEventListener("load", () => { 7 | function setup_toggle(type) { 8 | const toggle = document.getElementById("set_" + type); 9 | 10 | function on_change() { 11 | const rel = document.getElementById(type + "_rel"); 12 | if (toggle.checked) { 13 | rel.parentElement.style.opacity = "1"; 14 | } else { 15 | // $("#" + type + "_rel").attr("disabled", "disabled"); 16 | rel.parentElement.style.opacity = "0.4"; 17 | rel.value = document.querySelector(`#${type}_rel option:first-child`).value; 18 | rel.dispatchEvent(new Event("change")); 19 | } 20 | } 21 | 22 | toggle.addEventListener("change", on_change); 23 | on_change(); 24 | } 25 | 26 | setup_toggle("min"); 27 | setup_toggle("max"); 28 | }); 29 | -------------------------------------------------------------------------------- /migrations/versions/1fe2e44cf565_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1fe2e44cf565 4 | Revises: d73078c5d619 5 | Create Date: 2024-03-30 16:19:47.384716 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1fe2e44cf565' 14 | down_revision = 'd73078c5d619' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('user', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('github_user_id', sa.Integer(), nullable=True)) 22 | batch_op.create_unique_constraint("_user_github_user_id", ['github_user_id']) 23 | 24 | 25 | def downgrade(): 26 | with op.batch_alter_table('user', schema=None) as batch_op: 27 | batch_op.drop_constraint("_user_github_user_id", type_='unique') 28 | batch_op.drop_column('github_user_id') 29 | -------------------------------------------------------------------------------- /migrations/versions/dd27f1311a90_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: dd27f1311a90 4 | Revises: c141a63b2487 5 | Create Date: 2020-07-09 00:20:39.501355 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dd27f1311a90' 14 | down_revision = 'c141a63b2487' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('score_downloads', sa.Float(), nullable=False, server_default="0")) 22 | op.execute(text(""" 23 | UPDATE "package" SET "score_downloads"="score"; 24 | """)) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column('package', 'score_downloads') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /migrations/versions/d6ae9682c45f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: d6ae9682c45f 4 | Revises: 7ff57806ffd5 5 | Create Date: 2019-07-01 23:27:42.666877 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'd6ae9682c45f' 13 | down_revision = '7ff57806ffd5' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('user', sa.Column('donate_url', sa.String(length=255), nullable=True)) 21 | op.add_column('user', sa.Column('website_url', sa.String(length=255), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('user', 'website_url') 28 | op.drop_column('user', 'donate_url') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/dff4b87e4a76_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: dff4b87e4a76 4 | Revises: 3a24fc02365e 5 | Create Date: 2020-07-17 23:47:51.096874 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dff4b87e4a76' 14 | down_revision = '3a24fc02365e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('approved_at', sa.DateTime(), nullable=True, server_default=None)) 22 | 23 | op.execute(text(""" 24 | UPDATE package SET approved_at=created_at WHERE approved; 25 | """)) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_column('package', 'approved_at') 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/242fd82077bb_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 242fd82077bb 4 | Revises: 1acc6e90bbac 5 | Create Date: 2025-09-01 10:00:39.263576 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '242fd82077bb' 14 | down_revision = '1acc6e90bbac' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('report_attachment', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('created_at', sa.DateTime(), nullable=False), 23 | sa.Column('report_id', sa.String(length=24), nullable=False), 24 | sa.Column('url', sa.String(length=100), nullable=False), 25 | sa.ForeignKeyConstraint(['report_id'], ['report.id'], ), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | 29 | 30 | def downgrade(): 31 | op.drop_table('report_attachment') 32 | -------------------------------------------------------------------------------- /migrations/versions/17b303f33f68_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 17b303f33f68 4 | Revises: 96a01fe23389 5 | Create Date: 2021-12-20 19:48:58.571336 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '17b303f33f68' 14 | down_revision = '96a01fe23389' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | status = postgresql.ENUM('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate') 21 | status.create(op.get_bind()) 22 | 23 | op.add_column('package', sa.Column('dev_state', sa.Enum('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate'), nullable=True)) 24 | 25 | 26 | def downgrade(): 27 | op.drop_column('package', 'dev_state') 28 | -------------------------------------------------------------------------------- /app/blueprints/api/auth.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | from functools import wraps 6 | 7 | from flask import request, abort 8 | 9 | from app.models import APIToken 10 | from .support import error 11 | 12 | 13 | def is_api_authd(f): 14 | @wraps(f) 15 | def decorated_function(*args, **kwargs): 16 | token = None 17 | 18 | value = request.headers.get("authorization") 19 | if value is None: 20 | pass 21 | elif value[0:7].lower() == "bearer ": 22 | access_token = value[7:] 23 | if len(access_token) < 10: 24 | error(400, "API token is too short") 25 | 26 | token = APIToken.query.filter_by(access_token=access_token).first() 27 | if token is None: 28 | error(403, "Unknown API token") 29 | else: 30 | error(403, "Unsupported authentication method") 31 | 32 | return f(token=token, *args, **kwargs) 33 | 34 | return decorated_function 35 | -------------------------------------------------------------------------------- /migrations/versions/838081950f27_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 838081950f27 4 | Revises: 86512692b770 5 | Create Date: 2020-07-12 01:33:19.499459 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '838081950f27' 13 | down_revision = '86512692b770' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.execute(text(""" 20 | DELETE FROM provides AS t USING meta_package AS m WHERE t.metapackage_id = m.id AND NOT (m.name ~* '^[a-z0-9_]+$'); 21 | DELETE FROM dependency AS t USING meta_package AS m WHERE t.meta_package_id = m.id AND NOT (m.name ~* '^[a-z0-9_]+$'); 22 | DELETE FROM meta_package WHERE NOT (name ~* '^[a-z0-9_]+$'); 23 | """)) 24 | 25 | op.create_check_constraint("mp_name_valid", "meta_package", "name ~* '^[a-z0-9_]+$'") 26 | 27 | 28 | def downgrade(): 29 | op.drop_constraint("mp_name_valid", "meta_package", type_="check") 30 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /app/templates/packages/stats.html: -------------------------------------------------------------------------------- 1 | {% extends "packages/package_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Statistics") }} - {{ package.title }} 5 | {% endblock %} 6 | 7 | {% from "macros/stats.html" import render_package_stats, render_package_stats_js, 8 | render_package_selector, render_daterange_selector with context %} 9 | 10 | {% block scriptextra %} 11 | {{ render_package_stats_js() }} 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
    16 | 17 | 18 | {{ _("Download (.csv)") }} 19 | 20 | {{ render_daterange_selector(options, start or end) }} 21 | {{ render_package_selector(package.author, package=package) }} 22 |
    23 |

    {{ _("Statistics") }}

    24 | {{ render_package_stats(package.get_url('api.package_stats', start=start, end=end), package.downloads, start or end) }} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /migrations/versions/9689a71efe88_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9689a71efe88 4 | Revises: 3052712496e4 5 | Create Date: 2025-08-26 14:24:02.045713 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9689a71efe88' 14 | down_revision = '3052712496e4' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('report', schema=None) as batch_op: 21 | batch_op.alter_column('id', 22 | existing_type=sa.INTEGER(), 23 | type_=sa.String(length=24), 24 | existing_nullable=False) 25 | 26 | 27 | def downgrade(): 28 | with op.batch_alter_table('report', schema=None) as batch_op: 29 | batch_op.alter_column('id', 30 | existing_type=sa.String(length=24), 31 | type_=sa.INTEGER(), 32 | existing_nullable=False) 33 | -------------------------------------------------------------------------------- /app/blueprints/api/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | import json 6 | from flask import Blueprint 7 | 8 | from .support import error 9 | 10 | bp = Blueprint("api", __name__) 11 | 12 | 13 | from . import tokens, endpoints 14 | 15 | 16 | @bp.errorhandler(400) 17 | @bp.errorhandler(401) 18 | @bp.errorhandler(403) 19 | @bp.errorhandler(404) 20 | def handle_exception(e): 21 | """Return JSON instead of HTML for HTTP errors.""" 22 | # start with the correct headers and status code from the error 23 | response = e.get_response() 24 | # replace the body with JSON 25 | response.data = json.dumps({ 26 | "success": False, 27 | "code": e.code, 28 | "name": e.name, 29 | "description": e.description, 30 | }) 31 | response.content_type = "application/json" 32 | return response 33 | 34 | 35 | @bp.route("/api/") 36 | def page_not_found(path): 37 | error(404, "Endpoint or method not found") 38 | -------------------------------------------------------------------------------- /app/templates/tasks/view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %} 5 | {{ _("Task Failed") }} 6 | {% else %} 7 | {{ _("Working…") }} 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

    {{ self.title() }}

    13 |
    14 |

    15 |
    16 |
    17 |
    18 |
    19 | 20 | {% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %} 21 |
    {{ info.error }}
    22 | {% else %} 23 | 24 | 27 | {% endif %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /app/templates/packages/screenshot_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Edit screenshot") }} - {{ package.title }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 | {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %} 10 |
    11 |

    {{ _("Edit screenshot") }}

    12 | 13 | {{ form.hidden_tag() }} 14 | 15 | {{ render_field(form.title) }} 16 | 17 | {% if package.check_perm(current_user, "APPROVE_SCREENSHOT") %} 18 | {{ render_checkbox_field(form.approved) }} 19 | {% else %} 20 |

    {{ _("Approved") }}: {{ screenshot.approved }}

    21 | {% endif %} 22 | 23 | {{ render_submit_field(form.submit) }} 24 |
    25 | 26 | 27 | {{ screenshot.title }} 28 | 29 |
    30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /migrations/versions/16eb610b7751_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 16eb610b7751 4 | Revises: 76ff303f76d8 5 | Create Date: 2022-09-14 21:10:40.126876 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | from sqlalchemy_searchable import sync_trigger 14 | 15 | revision = '16eb610b7751' 16 | down_revision = '76ff303f76d8' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | conn = op.get_bind() 23 | 24 | options = {"weights": {"name": "A", "title": "B", "short_desc": "C"}} 25 | # sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc"], options=options) 26 | 27 | 28 | def downgrade(): 29 | conn = op.get_bind() 30 | 31 | options = {"weights": {"name": "A", "title": "B", "short_desc": "C", "desc": "D"}} 32 | # sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"], options=options) 33 | -------------------------------------------------------------------------------- /app/templates/modnames/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Mod Names") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |
    11 |
    12 | {{ _("Name") }} 13 |
    14 | 15 |
    16 | {{ _("Packages") }} 17 |
    18 |
    19 |
    20 | 21 | {% for pair in modnames %} 22 | {% set meta = pair[0] %} 23 | {% set count = pair[1] %} 24 | 26 |
    27 |
    28 | {{ meta.name }} 29 |
    30 | 31 |
    32 | {{ count }} 33 |
    34 |
    35 |
    36 | {% else %} 37 |
  • {{ _("No mod names found.") }}
  • 38 | {% endfor %} 39 |
    40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /migrations/versions/8d22def23c8b_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8d22def23c8b 4 | Revises: 42b14763c95e 5 | Create Date: 2020-12-10 22:23:32.291613 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8d22def23c8b' 14 | down_revision = 'a9c1c08bf956' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('cover_image_id', sa.Integer(), nullable=True)) 22 | op.create_foreign_key(None, 'package', 'package_screenshot', ['cover_image_id'], ['id']) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_constraint(None, 'package', type_='foreignkey') 29 | op.drop_column('package', 'cover_image_id') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /app/tests/unit/logic/test_graphs.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | import datetime 6 | 7 | from app.logic.graphs import flatten_data 8 | 9 | 10 | class DailyStat: 11 | date: datetime.date 12 | platform_minetest: int 13 | platform_other: int 14 | reason_new: int 15 | reason_dependency: int 16 | reason_update: int 17 | 18 | def __init__(self, date: str, x: int): 19 | self.date = datetime.date.fromisoformat(date) 20 | self.platform_minetest = x 21 | self.platform_other = 0 22 | self.reason_new = 0 23 | self.reason_dependency = 0 24 | self.reason_update = 0 25 | 26 | 27 | def test_flatten_data(): 28 | res = flatten_data([ 29 | DailyStat("2022-03-28", 3), 30 | DailyStat("2022-03-29", 10), 31 | DailyStat("2022-04-01", 5), 32 | DailyStat("2022-04-02", 1) 33 | ]) 34 | 35 | assert res["start"] == "2022-03-28" 36 | assert res["end"] == "2022-04-02" 37 | assert res["platform_minetest"] == [3, 10, 0, 0, 5, 1] 38 | -------------------------------------------------------------------------------- /app/templates/users/settings_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    5 |
    6 | 24 |
    25 |
    26 | {% block pane %} 27 | {% endblock %} 28 |
    29 |
    30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /app/templates/report/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title -%} 4 | Reports 5 | {%- endblock %} 6 | 7 | 8 | {% block content %} 9 | 10 |

    {{ self.title() }}

    11 | 12 | 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /app/templates/admin/tags/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if tag %} 5 | Edit {{ tag.get_translated().title }} 6 | {% else %} 7 | New tag 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | New Tag 13 | Back to list 14 | 15 | {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | 19 | {{ render_field(form.title) }} 20 | {{ render_field(form.description) }} 21 | {% if tag %} 22 | {{ render_field(form.name) }} 23 | {% endif %} 24 | {{ render_submit_field(form.submit) }} 25 | {% if tag %} 26 | 27 | View packages with tag 28 | 29 | {% endif %} 30 |
    31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /migrations/versions/8ee3cf3fb312_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8ee3cf3fb312 4 | Revises: e82c2141fae3 5 | Create Date: 2021-05-03 22:21:02.167758 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy import text 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8ee3cf3fb312' 14 | down_revision = 'e82c2141fae3' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column('user', 'email_confirmed_at', 21 | existing_type=postgresql.TIMESTAMP(), 22 | nullable=True) 23 | op.execute(text("""UPDATE "user" SET email_confirmed_at = NULL WHERE email_confirmed_at < '2016-01-01'::date""")) 24 | 25 | 26 | def downgrade(): 27 | op.alter_column('user', 'email_confirmed_at', 28 | existing_type=postgresql.TIMESTAMP(), 29 | nullable=False) 30 | op.execute( 31 | text("""UPDATE "user" SET email_confirmed_at = '2004-01-01'::date WHERE email_confirmed_at IS NULL""")) 32 | -------------------------------------------------------------------------------- /app/templates/api/list_tokens.html: -------------------------------------------------------------------------------- 1 | {% extends "users/settings_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("API Tokens | %(username)s", username=user.username) }} 5 | {% endblock %} 6 | 7 | {% block pane %} 8 | {{ _("Create") }} 9 | {{ _("API Documentation") }} 10 |

    {{ _("API Tokens") }}

    11 | 12 |
    13 | {% for token in user.tokens %} 14 | 15 | {% if token.client %} 16 | 17 | {{ _("Application") }} 18 | 19 | {% endif %} 20 | {{ token.name }} 21 | 22 | {% else %} 23 | 24 | {{ _("No tokens created") }} 25 | 26 | {% endfor %} 27 |
    28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /migrations/versions/aa6d21889d22_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: aa6d21889d22 4 | Revises: b254f55eadd2 5 | Create Date: 2018-05-29 18:28:28.540416 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'aa6d21889d22' 14 | down_revision = 'b254f55eadd2' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('user', 'password', 22 | existing_type=sa.VARCHAR(length=255), 23 | nullable=True, 24 | existing_server_default=sa.text("''")) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.alter_column('user', 'password', 31 | existing_type=sa.VARCHAR(length=255), 32 | nullable=False, 33 | existing_server_default=sa.text("''")) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/de004661c5e1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: de004661c5e1 4 | Revises: 605b3d74ada1 5 | Create Date: 2018-06-11 23:38:38.611039 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'de004661c5e1' 14 | down_revision = '605b3d74ada1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('watchers', 22 | sa.Column('user_id', sa.Integer(), nullable=False), 23 | sa.Column('thread_id', sa.Integer(), nullable=False), 24 | sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ), 25 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 26 | sa.PrimaryKeyConstraint('user_id', 'thread_id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('watchers') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/01f8d5de29e1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 01f8d5de29e1 4 | Revises: e571b3498f9e 5 | Create Date: 2022-02-13 10:12:20.150232 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '01f8d5de29e1' 14 | down_revision = 'e571b3498f9e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('user_ban', 21 | sa.Column('user_id', sa.Integer(), nullable=False), 22 | sa.Column('message', sa.UnicodeText(), nullable=False), 23 | sa.Column('banned_by_id', sa.Integer(), nullable=False), 24 | sa.Column('created_at', sa.DateTime(), nullable=False), 25 | sa.Column('expires_at', sa.DateTime(), nullable=True), 26 | sa.ForeignKeyConstraint(['banned_by_id'], ['user.id'], ), 27 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 28 | sa.PrimaryKeyConstraint('user_id') 29 | ) 30 | 31 | 32 | def downgrade(): 33 | op.drop_table('user_ban') 34 | -------------------------------------------------------------------------------- /utils/setup.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | import inspect 7 | import os 8 | import sys 9 | 10 | 11 | if not "FLASK_CONFIG" in os.environ: 12 | os.environ["FLASK_CONFIG"] = "../config.cfg" 13 | 14 | create_db = not (len(sys.argv) >= 2 and sys.argv[1].strip() == "-o") 15 | test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t" or not create_db 16 | 17 | # Allow finding the `app` module 18 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 19 | parentdir = os.path.dirname(currentdir) 20 | sys.path.insert(0,parentdir) 21 | 22 | from app.models import db 23 | from app.default_data import populate, populate_test_data 24 | 25 | from app import app 26 | with app.app_context(): 27 | if create_db: 28 | print("Creating database tables...") 29 | db.create_all() 30 | 31 | print("Filling database...") 32 | 33 | populate(db.session) 34 | if test_data: 35 | populate_test_data(db.session) 36 | 37 | db.session.commit() 38 | -------------------------------------------------------------------------------- /app/templates/users/change_set_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Set Password 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ _("Set Password") }}

    9 | 10 | {% from "macros/forms.html" import render_field, render_submit_field %} 11 |
    12 | {{ form.hidden_tag() }} 13 | 14 | {% if form.email and not current_user.email %} 15 | {{ render_field(form.email, tabindex=220, 16 | hint=_("Your email is needed to recover your account if you forget your password and to send (configurable) notifications. ") + 17 | _("Your email will never be shared with a third-party.")) }} 18 | {% endif %} 19 | 20 | {% if form.old_password %} 21 | {{ render_field(form.old_password, tabindex=230) }} 22 | {% endif %} 23 | 24 | {{ render_field(form.password, tabindex=230, hint=_("Must be at least 12 characters long.")) }} 25 | {{ render_field(form.password2, tabindex=240) }} 26 | 27 | {{ render_submit_field(form.submit, tabindex=280) }} 28 |
    29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /migrations/versions/1e08d7e4c15d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1e08d7e4c15d 4 | Revises: 9689a71efe88 5 | Create Date: 2025-08-26 14:43:30.501823 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1e08d7e4c15d' 14 | down_revision = '9689a71efe88' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | status = postgresql.ENUM('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory') 21 | status.create(op.get_bind()) 22 | with op.batch_alter_table('report', schema=None) as batch_op: 23 | batch_op.add_column(sa.Column('category', sa.Enum('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory'), nullable=False, server_default="OTHER")) 24 | 25 | 26 | def downgrade(): 27 | with op.batch_alter_table('report', schema=None) as batch_op: 28 | batch_op.drop_column('category') 29 | -------------------------------------------------------------------------------- /migrations/versions/81e0eb07a3cd_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 81e0eb07a3cd 4 | Revises: f30031f0b928 5 | Create Date: 2018-05-23 19:22:56.590653 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '81e0eb07a3cd' 14 | down_revision = 'f30031f0b928' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('package_release', 'url', 22 | existing_type=sa.VARCHAR(length=100), 23 | type_=sa.String(length=200), 24 | existing_nullable=False) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.alter_column('package_release', 'url', 31 | existing_type=sa.String(length=200), 32 | type_=sa.VARCHAR(length=100), 33 | existing_nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/9832944cd1e4_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9832944cd1e4 4 | Revises: 838081950f27 5 | Create Date: 2020-07-15 15:00:45.440381 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9832944cd1e4' 14 | down_revision = '838081950f27' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('thread_reply', 'comment', 22 | existing_type=sa.VARCHAR(length=500), 23 | type_=sa.String(length=2000), 24 | existing_nullable=False) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.alter_column('thread_reply', 'comment', 31 | existing_type=sa.String(length=2000), 32 | type_=sa.VARCHAR(length=500), 33 | existing_nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/e9f534df23a8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e9f534df23a8 4 | Revises: adad68a5e370 5 | Create Date: 2018-06-02 18:30:54.234366 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e9f534df23a8' 14 | down_revision = 'adad68a5e370' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('krock_forum_topic', 'link', 22 | existing_type=sa.VARCHAR(length=50), 23 | type_=sa.String(length=200), 24 | existing_nullable=False) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.alter_column('package_release', 'link', 31 | existing_type=sa.String(length=200), 32 | type_=sa.VARCHAR(length=50), 33 | existing_nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/f30031f0b928_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f30031f0b928 4 | Revises: 83622276d439 5 | Create Date: 2018-05-23 18:19:07.428378 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f30031f0b928' 14 | down_revision = '83622276d439' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('package_release', 'task_id', 22 | existing_type=sa.VARCHAR(length=32), 23 | type_=sa.String(length=37), 24 | existing_nullable=True) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.alter_column('package_release', 'task_id', 31 | existing_type=sa.String(length=37), 32 | type_=sa.VARCHAR(length=32), 33 | existing_nullable=True) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /app/flatpages/help/wtfpl.md: -------------------------------------------------------------------------------- 1 | title: WTFPL is a terrible license 2 | toc: False 3 | 4 | The use of WTFPL as a license is discouraged for multiple reasons. 5 | 6 | * **No Warranty disclaimer:** This could open you up to being sued.[1] 7 | * **Swearing:** This prevents settings like schools from using your content. 8 | * **Not OSI Approved:** Same as public domain? 9 | 10 | The Open Source Initiative chose not to approve the license as an open-source 11 | license, saying:[3] 12 | 13 | > It's no different from dedication to the public domain. 14 | > Author has submitted license approval request – author is free to make public domain dedication. 15 | > Although he agrees with the recommendation, Mr. Michlmayr notes that public domain doesn't exist in Europe. Recommend: Reject. 16 | 17 | ## Sources 18 | 19 | 1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html) 20 | 2. [FSF](https://www.gnu.org/licenses/license-list.en.html) 21 | 3. [OSI](https://opensource.org/meeting-minutes/minutes20090304) 22 | -------------------------------------------------------------------------------- /app/templates/oauth/list_clients.html: -------------------------------------------------------------------------------- 1 | {% extends "users/settings_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("OAuth2 Applications | %(username)s", username=user.username) }} 5 | {% endblock %} 6 | 7 | {% block pane %} 8 | {{ _("Create") }} 9 | {{ _("OAuth2 Documentation") }} 10 |

    {{ _("OAuth2 Applications") }}

    11 | 12 |
    13 | {% for client in user.clients %} 14 | 15 | {% if not client.approved %} 16 | {{ _("Unpublished") }} 17 | {% endif %} 18 | {{ client.title }} 19 | 20 | {% else %} 21 | 22 | {{ _("No applications created") }} 23 | 24 | {% endfor %} 25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /migrations/versions/06d23947e7ef_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 06d23947e7ef 4 | Revises: 5d7233cf8a00 5 | Create Date: 2020-12-05 20:30:12.166357 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '06d23947e7ef' 14 | down_revision = '5d7233cf8a00' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('email_subscription', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('email', sa.String(length=100), nullable=False), 24 | sa.Column('blacklisted', sa.Boolean(), nullable=False), 25 | sa.Column('token', sa.String(length=32), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('email_subscription') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /app/templates/emails/verify.html: -------------------------------------------------------------------------------- 1 | {% extends "emails/base.html" %} 2 | 3 | {% block content %} 4 |

    {{ _("Hello!") }}

    5 | 6 |

    7 | {{ _("This email has been sent to you because someone (hopefully you) has entered your email address as a user's email.") }} 8 |

    9 | 10 |

    11 | {{ _("If it wasn't you, then just delete this email.") }} 12 |

    13 | 14 |

    15 | {{ _("If this was you, then please click this link to confirm the address:") }} 16 |

    17 | 18 | 19 | {{ _("Confirm Email Address") }} 20 | 21 | 22 |

    23 | {{ _("Or paste this into your browser:") }} 24 | {{ abs_url_for('users.verify_email', token=token) }} 25 |

    26 | 27 | {% endblock %} 28 | 29 | {% block footer %} 30 | {{ _("You are receiving this email because someone (hopefully you) entered your email address as a user's email.") }} 31 |
    32 | 33 | {{ _("Unsubscribe") }} 34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /migrations/versions/7828535fe339_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7828535fe339 4 | Revises: 52cf6746f255 5 | Create Date: 2023-11-07 22:51:39.450652 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7828535fe339' 14 | down_revision = '52cf6746f255' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table('collection', schema=None) as batch_op: 21 | batch_op.add_column(sa.Column('pinned', sa.Boolean(), nullable=False, server_default="false")) 22 | 23 | with op.batch_alter_table('oauth_client', schema=None) as batch_op: 24 | batch_op.add_column(sa.Column('is_clientside', sa.Boolean(), nullable=False, server_default="false")) 25 | 26 | 27 | def downgrade(): 28 | with op.batch_alter_table('oauth_client', schema=None) as batch_op: 29 | batch_op.drop_column('is_clientside') 30 | 31 | with op.batch_alter_table('collection', schema=None) as batch_op: 32 | batch_op.drop_column('pinned') 33 | -------------------------------------------------------------------------------- /app/templates/zipgrep/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Search in Package Releases") }} 5 | {% endblock %} 6 | 7 | {% block query_hint %} 8 | 9 | POSIX Extended Regular Expressions 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

    {{ self.title() }}

    15 | {% from "macros/forms.html" import render_field, render_submit_field %} 16 |
    17 | {{ form.hidden_tag() }} 18 | {{ render_field(form.query, hint=self.query_hint()) }} 19 | {{ render_field(form.file_filter, hint="Supports wildcards and regex") }} 20 | {{ render_field(form.type, hint=_("Use shift to select multiple. Leave selection empty to match any type.")) }} 21 | {{ render_submit_field(form.submit, tabindex=180) }} 22 |
    23 | 24 |

    25 | For more information, see ZipGrep's man page. 26 |

    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /app/templates/macros/pagination.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, url_set_query) %} 2 |
      3 | {% set prev_url = url_set_query(page=pagination.prev_num) if pagination.has_prev %} 4 | {% set next_url = url_set_query(page=pagination.next_num) if pagination.has_next %} 5 | 6 |
    • 7 | « 8 |
    • 9 | 10 | {%- for page in pagination.iter_pages() %} 11 | {% if page %} 12 |
    • 13 | 15 | {{ page }} 16 | 17 |
    • 18 | {% else %} 19 |
    • 20 | 21 |
    • 22 | {% endif %} 23 | {%- endfor %} 24 | 25 |
    • 26 | » 27 |
    • 28 |
    29 | {% endmacro %} 30 | -------------------------------------------------------------------------------- /app/templates/emails/notification.html: -------------------------------------------------------------------------------- 1 | {% extends "emails/base.html" %} 2 | 3 | {% block content %} 4 |

    5 | {{ notification.title }} 6 |

    7 | 8 |

    9 | {% if notification.package %} 10 | {{ _("From %(username)s and on package %(package)s.", 11 | username=notification.causer.username, package=notification.package.title) }} 12 | {% else %} 13 | {{ _("From %(username)s.", username=notification.causer.username) }} 14 | {% endif %} 15 |

    16 | 17 |

    18 | 19 | {{ _("View Notification") }} 20 | 21 |

    22 | 23 | {% endblock %} 24 | 25 | {% block footer %} 26 | {{ _("You are receiving this email because you are a registered user of ContentDB, and have email notifications enabled.") }} 27 |
    28 | 29 | 30 | {{ _("Manage your preferences") }} 31 | 32 | | 33 | 34 | {{ _("Unsubscribe") }} 35 |
    36 | 37 | {{ notification.type.this_is }} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /migrations/versions/3052712496e4_.py: -------------------------------------------------------------------------------- 1 | from alembic import op 2 | import sqlalchemy as sa 3 | from sqlalchemy.dialects import postgresql 4 | 5 | # revision identifiers, used by Alembic. 6 | revision = '3052712496e4' 7 | down_revision = '663521dfe86d' 8 | branch_labels = None 9 | depends_on = None 10 | 11 | 12 | def upgrade(): 13 | # ### commands auto generated by Alembic - please adjust! ### 14 | op.create_table('report', 15 | sa.Column('id', sa.Integer(), nullable=False), 16 | sa.Column('created_at', sa.DateTime(), nullable=False), 17 | sa.Column('user_id', sa.Integer(), nullable=True), 18 | sa.Column('thread_id', sa.Integer(), nullable=True), 19 | sa.Column('url', sa.String(), nullable=True), 20 | sa.Column('title', sa.Unicode(length=300), nullable=False), 21 | sa.Column('message', sa.UnicodeText(), nullable=False), 22 | sa.Column('is_resolved', sa.Boolean(), nullable=False), 23 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 24 | sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('report') 31 | -------------------------------------------------------------------------------- /migrations/versions/e571b3498f9e_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e571b3498f9e 4 | Revises: 3710e5fbbe87 5 | Create Date: 2022-02-01 19:30:59.537512 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e571b3498f9e' 14 | down_revision = '3710e5fbbe87' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('package_game_support', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('package_id', sa.Integer(), nullable=False), 23 | sa.Column('game_id', sa.Integer(), nullable=False), 24 | sa.Column('supports', sa.Boolean(), nullable=False), 25 | sa.Column('confidence', sa.Integer(), nullable=False), 26 | sa.ForeignKeyConstraint(['game_id'], ['package.id'], ), 27 | sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), 28 | sa.PrimaryKeyConstraint('id'), 29 | sa.UniqueConstraint('game_id', 'package_id', name='_package_game_support_uc') 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table('package_game_support') 35 | -------------------------------------------------------------------------------- /migrations/versions/51be0401bb85_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 51be0401bb85 4 | Revises: d4262fb15b37 5 | Create Date: 2021-07-24 00:25:04.706191 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '51be0401bb85' 14 | down_revision = 'd4262fb15b37' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('package_alias', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('package_id', sa.Integer(), nullable=False), 24 | sa.Column('author', sa.String(length=50), nullable=False), 25 | sa.Column('name', sa.String(length=100), nullable=False), 26 | sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), 27 | sa.PrimaryKeyConstraint('id') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('package_alias') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /migrations/versions/ea83ce985e55_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ea83ce985e55 4 | Revises: 16eb610b7751 5 | Create Date: 2022-11-05 22:09:50.875158 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ea83ce985e55' 14 | down_revision = '16eb610b7751' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('package_daily_stats', 21 | sa.Column('package_id', sa.Integer(), nullable=False), 22 | sa.Column('date', sa.Date(), nullable=False), 23 | sa.Column('platform_minetest', sa.Integer(), nullable=False), 24 | sa.Column('platform_other', sa.Integer(), nullable=False), 25 | sa.Column('reason_new', sa.Integer(), nullable=False), 26 | sa.Column('reason_dependency', sa.Integer(), nullable=False), 27 | sa.Column('reason_update', sa.Integer(), nullable=False), 28 | sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), 29 | sa.PrimaryKeyConstraint('package_id', 'date') 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table('package_daily_stats') 35 | -------------------------------------------------------------------------------- /app/templates/admin/restore.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Restore 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 |

    Restore Package

    10 | 11 |
    12 | 13 |
    14 | 22 |
    23 | 24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /migrations/versions/f565dde93553_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f565dde93553 4 | Revises: 4585ce5147b8 5 | Create Date: 2020-12-15 21:49:19.190893 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f565dde93553' 14 | down_revision = '4585ce5147b8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_update_config', sa.Column('ref', sa.String(length=41), nullable=True)) 21 | op.add_column('user_notification_preferences', sa.Column('pref_bot', sa.Integer(), nullable=True, server_default=None)) 22 | op.execute(text("""UPDATE user_notification_preferences SET pref_bot=pref_new_thread""")) 23 | op.alter_column('user_notification_preferences', 'pref_bot', 24 | existing_type=sa.INTEGER(), 25 | nullable=False) 26 | 27 | op.execute(text("COMMIT")) 28 | op.execute(text("ALTER TYPE notificationtype ADD VALUE 'BOT'")) 29 | 30 | 31 | def downgrade(): 32 | op.drop_column('user_notification_preferences', 'pref_bot') 33 | op.drop_column('package_update_config', 'ref') 34 | -------------------------------------------------------------------------------- /migrations/versions/3f5836a3df5c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3f5836a3df5c 4 | Revises: b3c7ff6655af 5 | Create Date: 2020-12-04 22:30:33.420071 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3f5836a3df5c' 14 | down_revision = 'b3c7ff6655af' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column('user', 'password', 21 | existing_type=sa.VARCHAR(length=255), 22 | nullable=True, 23 | existing_server_default=sa.text("''::character varying")) 24 | 25 | op.execute(text(""" 26 | UPDATE "user" SET password=NULL WHERE password='' 27 | """)) 28 | op.create_check_constraint("CK_password", "user", 29 | "password IS NULL OR password != ''") 30 | 31 | 32 | def downgrade(): 33 | op.drop_constraint("CK_password", "user", type_="check") 34 | op.alter_column('user', 'password', 35 | existing_type=sa.VARCHAR(length=255), 36 | nullable=False, 37 | existing_server_default=sa.text("''::character varying")) 38 | -------------------------------------------------------------------------------- /migrations/versions/81de25b72f66_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 81de25b72f66 4 | Revises: c154912eaa0c 5 | Create Date: 2020-12-05 03:38:42.004388 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | from sqlalchemy.dialects import postgresql 14 | 15 | revision = '81de25b72f66' 16 | down_revision = 'c154912eaa0c' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | status = postgresql.ENUM('OTHER', 'PACKAGE_EDIT', 'PACKAGE_APPROVAL', 'NEW_THREAD', 'NEW_REVIEW', 'THREAD_REPLY', 'MAINTAINER', 'EDITOR_ALERT', 'EDITOR_MISC', name='notificationtype') 23 | status.create(op.get_bind()) 24 | 25 | op.add_column('notification', sa.Column('emailed', sa.Boolean(), nullable=False, server_default="true")) 26 | op.add_column('notification', sa.Column('type', sa.Enum('OTHER', 'PACKAGE_EDIT', 'PACKAGE_APPROVAL', 'NEW_THREAD', 'NEW_REVIEW', 'THREAD_REPLY', 'MAINTAINER', 'EDITOR_ALERT', 'EDITOR_MISC', name='notificationtype'), nullable=False, server_default="OTHER")) 27 | 28 | 29 | def downgrade(): 30 | op.drop_column('notification', 'type') 31 | op.drop_column('notification', 'emailed') 32 | -------------------------------------------------------------------------------- /migrations/versions/dabd7ab14339_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: dabd7ab14339 4 | Revises: aa53b4d36c50 5 | Create Date: 2023-04-15 01:18:53.212673 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dabd7ab14339' 14 | down_revision = 'aa53b4d36c50' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('package_review', sa.Column('rating', sa.Integer(), nullable=True)) 21 | op.execute(""" 22 | UPDATE package_review SET rating = CASE 23 | WHEN recommends THEN 5 24 | ELSE 1 25 | END; 26 | """) 27 | op.drop_column('package_review', 'recommends') 28 | op.alter_column('package_review', 'rating', nullable=False) 29 | 30 | 31 | def downgrade(): 32 | op.add_column('package_review', sa.Column('recommends', sa.BOOLEAN(), autoincrement=False, nullable=True)) 33 | op.execute(""" 34 | UPDATE package_review SET recommends = rating >= 3; 35 | """) 36 | op.drop_column('package_review', 'rating') 37 | op.alter_column('package_review', 'recommends', nullable=False) 38 | -------------------------------------------------------------------------------- /migrations/versions/fd25bf3e57c3_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: fd25bf3e57c3 4 | Revises: d6ae9682c45f 5 | Create Date: 2019-11-26 23:43:47.476346 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'fd25bf3e57c3' 13 | down_revision = 'd6ae9682c45f' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('api_token', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('access_token', sa.String(length=34), nullable=True), 23 | sa.Column('name', sa.String(length=100), nullable=False), 24 | sa.Column('owner_id', sa.Integer(), nullable=False), 25 | sa.Column('created_at', sa.DateTime(), nullable=False), 26 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ), 27 | sa.PrimaryKeyConstraint('id'), 28 | sa.UniqueConstraint('access_token') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('api_token') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /app/flatpages/help.md: -------------------------------------------------------------------------------- 1 | title: Help 2 | toc: False 3 | 4 | 5 | ## Rules 6 | 7 | * [Terms of Service](/terms/) 8 | * [Package Inclusion Policy and Guidance](/policy_and_guidance/) 9 | 10 | ## General Help 11 | 12 | * [Frequently Asked Questions](faq/) 13 | * [Installing content](installing/) 14 | * [Content Ratings and Flags](content_flags/) 15 | * [Non-free Licenses](non_free/) 16 | * [Why WTFPL is a terrible license](wtfpl/) 17 | * [Ranks and Permissions](ranks_permissions/) 18 | * [Contact Us](contact_us/) 19 | * [Top Packages Algorithm](top_packages/) 20 | * [Featured Packages](featured/) 21 | * [Feeds](feeds/) 22 | 23 | ## Help for Package Authors 24 | 25 | * [Package Inclusion Policy and Guidance](/policy_and_guidance/) 26 | * [Copyright Guide](copyright/) 27 | * [Git Update Detection](update_config/) 28 | * [Creating Releases using Webhooks](release_webhooks/) 29 | * [Package Configuration and Releases Guide](package_config/) 30 | * [Supported Games](game_support/) 31 | * [Creating an appealing ContentDB page](appealing_page/) 32 | 33 | 34 | ## Help for Specific User Ranks 35 | 36 | * [Editors](editors/) 37 | 38 | ## APIs 39 | 40 | * [API](api/) 41 | * [OAuth2 Applications](oauth/) 42 | * [Prometheus Metrics](metrics/) 43 | -------------------------------------------------------------------------------- /app/public/static/js/release_minmax.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | window.addEventListener("load", () => { 7 | const min = document.getElementById("min_rel"); 8 | const max = document.getElementById("max_rel"); 9 | const none = parseInt(document.querySelector("#min_rel option:first-child").value); 10 | const latestMax = parseInt(document.querySelector("#max_rel option:last-child").value); 11 | const warningMinMax = document.getElementById("minmax_warning"); 12 | const warningMax = document.getElementById("latest_release"); 13 | 14 | function ver_check() { 15 | const minv = parseInt(min.value); 16 | const maxv = parseInt(max.value); 17 | if (minv != none && maxv != none && minv > maxv) { 18 | warningMinMax.classList.remove("d-none"); 19 | } else { 20 | warningMinMax.classList.add("d-none"); 21 | } 22 | 23 | if (maxv == latestMax) { 24 | warningMax.classList.remove("d-none"); 25 | } else { 26 | warningMax.classList.add("d-none"); 27 | } 28 | } 29 | 30 | min.addEventListener("change", ver_check); 31 | max.addEventListener("change", ver_check); 32 | ver_check(); 33 | }); 34 | -------------------------------------------------------------------------------- /migrations/versions/886c92dc6eaa_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 886c92dc6eaa 4 | Revises: 8d22def23c8b 5 | Create Date: 2020-12-15 16:38:54.114559 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '886c92dc6eaa' 14 | down_revision = '8d22def23c8b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('package_update_config', 22 | sa.Column('package_id', sa.Integer(), nullable=False), 23 | sa.Column('last_commit', sa.String(length=41), nullable=True), 24 | sa.Column('trigger', sa.Enum('COMMIT', 'TAG', name='packageupdatetrigger'), nullable=False), 25 | sa.Column('make_release', sa.Boolean(), nullable=False), 26 | sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), 27 | sa.PrimaryKeyConstraint('package_id') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('package_update_config') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /config.example.cfg: -------------------------------------------------------------------------------- 1 | USER_APP_NAME = "ContentDB" 2 | SERVER_NAME = "localhost:5123" 3 | BASE_URL = "http://" + SERVER_NAME 4 | 5 | SECRET_KEY = "" 6 | WTF_CSRF_SECRET_KEY = "" 7 | 8 | SQLALCHEMY_DATABASE_URI = "postgresql://contentdb:password@db:5432/contentdb" 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | 11 | GITHUB_CLIENT_ID = "" 12 | GITHUB_CLIENT_SECRET = "" 13 | 14 | # Optional, used for an admin action - import user ids from GitHub 15 | GITHUB_API_TOKEN = "" 16 | 17 | REDIS_URL = 'redis://redis:6379' 18 | CELERY_BROKER_URL = 'redis://redis:6379' 19 | CELERY_RESULT_BACKEND = 'redis://redis:6379' 20 | 21 | MAIL_USERNAME = "" 22 | MAIL_PASSWORD = "" 23 | USER_EMAIL_SENDER_NAME = "" 24 | USER_EMAIL_SENDER_EMAIL = "" 25 | MAIL_DEFAULT_SENDER = "" 26 | MAIL_REPLY_TO = None 27 | MAIL_SERVER = "" 28 | MAIL_PORT = 587 29 | MAIL_USE_TLS = True 30 | 31 | UPLOAD_DIR = "/var/cdb/uploads/" 32 | THUMBNAIL_DIR = "/var/cdb/thumbnails/" 33 | 34 | DISCORD_WEBHOOK_FEED = None 35 | DISCORD_WEBHOOK_QUEUE = None 36 | 37 | TEMPLATES_AUTO_RELOAD = False 38 | LOG_SQL = False 39 | 40 | ENABLE_GIT_UPDATE_DETECTION = True 41 | 42 | BLOCKED_DOMAINS = [] 43 | LINK_CHECKER_IGNORED_URLS = ["liberapay.com"] 44 | 45 | ADMIN_CONTACT_URL = "" 46 | MONITORING_URL = None 47 | -------------------------------------------------------------------------------- /migrations/versions/097ce5d114d9_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 097ce5d114d9 4 | Revises: 1fe2e44cf565 5 | Create Date: 2024-06-08 09:59:23.084979 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "097ce5d114d9" 14 | down_revision = "1fe2e44cf565" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | with op.batch_alter_table("package_review", schema=None) as batch_op: 21 | batch_op.add_column(sa.Column("language_id", sa.String(), nullable=True, default=None)) 22 | batch_op.alter_column("package_id", 23 | existing_type=sa.INTEGER(), 24 | nullable=False) 25 | batch_op.create_foreign_key("package_review_language", "language", ["language_id"], ["id"]) 26 | 27 | 28 | def downgrade(): 29 | with op.batch_alter_table("package_review", schema=None) as batch_op: 30 | batch_op.drop_constraint("package_review_language", type_="foreignkey") 31 | batch_op.alter_column("package_id", 32 | existing_type=sa.INTEGER(), 33 | nullable=True) 34 | batch_op.drop_column("language_id") 35 | -------------------------------------------------------------------------------- /app/templates/packages/package_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block container %} 4 |
    5 | {% if tabs %} 6 |
    7 |
    8 | 28 |
    29 |
    30 | {% endif %} 31 | 32 | {{ self.content() }} 33 | 34 | {% if tabs %} 35 |
    36 |
    37 | {% endif %} 38 |
    39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /migrations/versions/a337bcc165c0_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: a337bcc165c0 4 | Revises: f565dde93553 5 | Create Date: 2021-01-29 21:30:37.277197 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a337bcc165c0' 14 | down_revision = 'f565dde93553' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package_update_config', sa.Column('outdated_at', sa.DateTime(), nullable=True)) 22 | op.add_column('package_update_config', sa.Column('last_tag', sa.String(length=41), nullable=True)) 23 | op.drop_column('package_update_config', 'outdated') 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.add_column('package_update_config', sa.Column('outdated', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False)) 30 | op.drop_column('package_update_config', 'outdated_at') 31 | op.drop_column('package_update_config', 'last_tag') 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /app/utils/url.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | import urllib.parse as urlparse 6 | from typing import Optional, Dict, List 7 | 8 | 9 | def url_set_query(url: str, params: Dict[str, str]) -> str: 10 | url_parts = list(urlparse.urlparse(url)) 11 | query = dict(urlparse.parse_qsl(url_parts[4])) 12 | query.update(params) 13 | 14 | url_parts[4] = urlparse.urlencode(query) 15 | return urlparse.urlunparse(url_parts) 16 | 17 | 18 | def url_get_query(parsed_url: urlparse.ParseResult) -> Dict[str, List[str]]: 19 | return urlparse.parse_qs(parsed_url.query) 20 | 21 | 22 | def get_youtube_id(url: str) -> Optional[str]: 23 | parsed = urlparse.urlparse(url) 24 | if (parsed.netloc == "www.youtube.com" or parsed.netloc == "youtube.com") and parsed.path == "/watch": 25 | video_id = url_get_query(parsed).get("v", [None])[0] 26 | if video_id: 27 | return video_id 28 | 29 | elif parsed.netloc == "youtu.be": 30 | return parsed.path[1:] 31 | 32 | return None 33 | 34 | 35 | def clean_youtube_url(url: str) -> Optional[str]: 36 | id_ = get_youtube_id(url) 37 | if id_: 38 | return url_set_query("https://www.youtube.com/watch", {"v": id_}) 39 | 40 | return None 41 | -------------------------------------------------------------------------------- /app/scss/lato.scss: -------------------------------------------------------------------------------- 1 | /* lato-regular - latin_latin-ext */ 2 | @font-face { 3 | font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 4 | font-family: 'Lato'; 5 | font-style: normal; 6 | font-weight: 400; 7 | src: url('/static/fonts/lato-v24-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 8 | } 9 | 10 | /* lato-italic - latin_latin-ext */ 11 | @font-face { 12 | font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 13 | font-family: 'Lato'; 14 | font-style: italic; 15 | font-weight: 400; 16 | src: url('/static/fonts/lato-v24-latin_latin-ext-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 17 | } 18 | 19 | /* lato-700 - latin_latin-ext */ 20 | @font-face { 21 | font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 22 | font-family: 'Lato'; 23 | font-style: normal; 24 | font-weight: 700; 25 | src: url('/static/fonts/lato-v24-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 26 | } 27 | -------------------------------------------------------------------------------- /migrations/versions/adad68a5e370_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: adad68a5e370 4 | Revises: d0bec9e5698e 5 | Create Date: 2018-06-02 18:23:18.123340 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'adad68a5e370' 14 | down_revision = 'd0bec9e5698e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('krock_forum_topic', 22 | sa.Column('topic_id', sa.Integer(), autoincrement=False, nullable=False), 23 | sa.Column('author_id', sa.Integer(), nullable=False), 24 | sa.Column('ttype', sa.Integer(), nullable=False), 25 | sa.Column('title', sa.String(length=200), nullable=False), 26 | sa.Column('name', sa.String(length=30), nullable=True), 27 | sa.Column('link', sa.String(length=50), nullable=True), 28 | sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), 29 | sa.PrimaryKeyConstraint('topic_id') 30 | ) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_table('krock_forum_topic') 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /app/templates/modnames/view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ modname.name }} - {{ _("Mod Names") }} 5 | {% endblock %} 6 | 7 | {% from "macros/packagegridtile.html" import render_pkggrid %} 8 | 9 | {% block content %} 10 |

    {{ _("Mod Name \"%(name)s\"", name=modname.name) }}

    11 | 12 |

    {{ _("Provided By") }}

    13 | 14 |

    {{ _("Mods") }}

    15 | {{ render_pkggrid(modname.packages.filter_by(type="MOD", state="APPROVED").all()) }} 16 | 17 |

    {{ _("Games") }}

    18 | {{ render_pkggrid(modname.packages.filter_by(type="GAME", state="APPROVED").all()) }} 19 | 20 | {% if similar_topics %} 21 |

    {{ _("Forum Topics") }}

    22 | 33 | {% endif %} 34 | 35 |

    {{ _("Required By") }}

    36 | {{ render_pkggrid(dependers) }} 37 | 38 |

    {{ _("Optionally Used By") }}

    39 | {{ render_pkggrid(optional_dependers) }} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /migrations/versions/cb6ab141c522_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: cb6ab141c522 4 | Revises: 7a48dbd05780 5 | Create Date: 2020-07-08 21:03:51.856561 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy import orm, text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'cb6ab141c522' 14 | down_revision = '7a48dbd05780' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('maintainers', 22 | sa.Column('user_id', sa.Integer(), nullable=False), 23 | sa.Column('package_id', sa.Integer(), nullable=False), 24 | sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), 25 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 26 | sa.PrimaryKeyConstraint('user_id', 'package_id') 27 | ) 28 | 29 | bind = op.get_bind() 30 | session = orm.Session(bind=bind) 31 | 32 | op.execute(text('INSERT INTO maintainers (package_id, user_id) SELECT id, author_id FROM package;')) 33 | 34 | session.commit() 35 | 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade(): 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table('maintainers') 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /app/templates/todo/outdated.html: -------------------------------------------------------------------------------- 1 | {% extends "todo/todo_base.html" %} 2 | 3 | {% block title %} 4 | {{ _("All Outdated packages") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 32 |
    33 | 34 | {% from "macros/todo.html" import render_outdated_packages %} 35 | {{ render_outdated_packages(outdated_packages, current_user) }} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /migrations/versions/aa6d7b595a94_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: aa6d7b595a94 4 | Revises: aa6d21889d22 5 | Create Date: 2018-05-29 20:09:56.647358 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import text 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'aa6d7b595a94' 14 | down_revision = 'aa6d21889d22' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('package', sa.Column('media_license_id', sa.Integer())) 22 | op.execute(text('UPDATE package SET media_license_id=license_id')) 23 | op.alter_column('package', 'media_license_id', nullable=False) 24 | op.alter_column('package', 'license_id', existing_type=sa.INTEGER(), nullable=False) 25 | op.create_foreign_key(None, 'package', 'license', ['media_license_id'], ['id']) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('package', 'license_id', 32 | existing_type=sa.INTEGER(), 33 | nullable=True) 34 | op.drop_column('package', 'media_license_id') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /app/blueprints/translate/__init__.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | 6 | from flask import Blueprint, render_template, request 7 | from sqlalchemy import or_ 8 | 9 | from app.models import Package, PackageState, db, PackageTranslation 10 | 11 | bp = Blueprint("translate", __name__) 12 | 13 | 14 | @bp.route("/translate/") 15 | def translate(): 16 | query = Package.query.filter( 17 | Package.state == PackageState.APPROVED, 18 | or_( 19 | Package.translation_url.is_not(None), 20 | Package.translations.any(PackageTranslation.language_id != "en") 21 | )) 22 | 23 | has_langs = request.args.getlist("has_lang") 24 | for lang in has_langs: 25 | query = query.filter(Package.translations.any(PackageTranslation.language_id == lang)) 26 | 27 | not_langs = request.args.getlist("not_lang") 28 | for lang in not_langs: 29 | query = query.filter(~Package.translations.any(PackageTranslation.language_id == lang)) 30 | 31 | supports_translation = (query 32 | .order_by(Package.translation_url.is_(None), db.desc(Package.score)) 33 | .all()) 34 | 35 | return render_template("translate/index.html", 36 | supports_translation=supports_translation, has_langs=has_langs, not_langs=not_langs) 37 | -------------------------------------------------------------------------------- /app/templates/admin/update_config.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Update config 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 |

    Failing update configs

    11 | 12 |
    13 | {% for package in failing_packages %} 14 |
    15 |
    16 | 21 |
    22 | Last success: 23 | {% if package.update_config.last_checked_at %} 24 | {{ package.update_config.last_checked_at | datetime }} 25 | {% else %} 26 | never 27 | {% endif %} 28 |
    29 | 37 |
    38 |
    39 | {% else %} 40 |
    41 | No failing packages 42 |
    43 | {% endfor %} 44 |
    45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /migrations/versions/daa040b727b2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: daa040b727b2 4 | Revises: 097ce5d114d9 5 | Create Date: 2024-06-22 13:57:51.857616 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "daa040b727b2" 13 | down_revision = "097ce5d114d9" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | with op.batch_alter_table("package_release", schema=None) as batch_op: 20 | batch_op.add_column(sa.Column("name", sa.String(length=30), nullable=False, server_default="")) 21 | batch_op.add_column(sa.Column("release_notes", sa.UnicodeText(), nullable=True)) 22 | batch_op.alter_column("releaseDate", nullable=False, new_column_name="created_at") 23 | 24 | op.execute(""" 25 | UPDATE package_release SET name = title WHERE length(title) <= 30; 26 | UPDATE package_release SET name = TO_CHAR(created_at, 'YYYY-MM-DD') WHERE name = ''; 27 | """) 28 | 29 | 30 | 31 | def downgrade(): 32 | with op.batch_alter_table("package_release", schema=None) as batch_op: 33 | batch_op.alter_column("created_at", nullable=False, new_column_name="releaseDate") 34 | batch_op.drop_column("release_notes") 35 | batch_op.drop_column("name") 36 | -------------------------------------------------------------------------------- /app/templates/admin/warnings/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Warnings") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ _("New Warning") }} 9 | 10 |

    {{ _("Warnings") }}

    11 | 12 |

    13 | Also see Package Flags. 14 |

    15 | 16 |
    17 |
    18 |
    19 |
    20 | {{ _("Name") }} 21 |
    22 | 23 |
    24 | {{ _("Description") }} 25 |
    26 | 27 |
    28 | {{ _("Packages") }} 29 |
    30 |
    31 |
    32 | 33 | {% for t in warnings %} 34 | 36 |
    37 |
    38 | {{ t.title }} 39 |
    40 | 41 |
    42 | {{ t.description }} 43 |
    44 | 45 |
    46 | {{ t.packages | count }} 47 |
    48 |
    49 |
    50 | {% endfor %} 51 |
    52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /app/public/static/js/video_embed.js: -------------------------------------------------------------------------------- 1 | // @author rubenwardy 2 | // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later 3 | 4 | "use strict"; 5 | 6 | document.querySelectorAll(".video-embed").forEach(ele => { 7 | try { 8 | const href = ele.getAttribute("href"); 9 | const url = new URL(href); 10 | 11 | if (url.host == "www.youtube.com") { 12 | ele.addEventListener("click", () => { 13 | ele.parentNode.classList.add("d-block"); 14 | ele.classList.add("ratio"); 15 | ele.classList.add("ratio-16x9"); 16 | ele.innerHTML = ` 17 | `; 21 | 22 | const embedURL = new URL("https://www.youtube.com/"); 23 | embedURL.pathname = "/embed/" + url.searchParams.get("v"); 24 | embedURL.searchParams.set("autoplay", "1"); 25 | 26 | const iframe = ele.children[0]; 27 | iframe.setAttribute("src", embedURL); 28 | }); 29 | 30 | ele.setAttribute("data-src", href); 31 | ele.removeAttribute("href"); 32 | 33 | ele.querySelector(".label").innerText = "YouTube"; 34 | } 35 | } catch (e) { 36 | console.error(url); 37 | return; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /app/templates/todo/mtver_support.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Packages not supporting %(rel)s", rel=current_stable.name) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 33 |
    34 | 35 | 36 | {% from "macros/todo.html" import render_mtsupport_packages %} 37 | {{ render_mtsupport_packages(packages, current_user) }} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /migrations/versions/2f3c3597c78d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2f3c3597c78d 4 | Revises: 9ec17b558413 5 | Create Date: 2019-01-29 02:43:08.865695 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy_searchable import sync_trigger 11 | from sqlalchemy_utils.types import TSVectorType 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '2f3c3597c78d' 15 | down_revision = '9ec17b558413' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.alter_column('package', 'shortDesc', nullable=False, new_column_name='short_desc') 23 | op.add_column('package', sa.Column('search_vector', TSVectorType("title", "short_desc", "desc"), nullable=True)) 24 | op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin') 25 | 26 | conn = op.get_bind() 27 | # sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"]) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_index('ix_package_search_vector', table_name='package') 34 | op.drop_column('package', 'search_vector') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /app/templates/report/report_received.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title -%} 4 | {{ _("We have received your report") }} 5 | {%- endblock %} 6 | 7 | 8 | {% block content %} 9 | 10 |

    {{ self.title() }}

    11 | 12 |

    13 | {{ _("We aim to resolve your report quickly.") }} 14 |

    15 | 16 |

    17 | {{ _("If the report is about illegal or harmful content, we aim to resolve within 48 hours.") }} 18 | {{ _("If we find the content to be infringing, we will remove it and may warn or suspend the user.") }} 19 |

    20 | 21 | {% if report.thread %} 22 |

    23 | {{ _("A private thread has been created for this report. You can use it to communicate with ContentDB staff and receive updates about the report.") }} 24 |

    25 | {% else %} 26 |

    27 | {{ _("Due to limited resources, we may not contact you further about the report unless we need clarification.") }} 28 |

    29 |

    30 | {{ _("For future reference, use report id: %(report_id)s.", report_id=report.id) }} 31 |

    32 | {% endif %} 33 | 34 |

    35 | {% if report.thread %} 36 | {{ _("View thread") }} 37 | {% endif %} 38 | {{ _("Back to home") }} 39 |

    40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /app/templates/packages/advanced_search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ _("Advanced Search") }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    {{ self.title() }}

    9 | 10 | {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %} 11 |
    12 | {{ render_field(form.q) }} 13 | {{ render_field(form.type, hint=_("Use shift to select multiple. Leave selection empty to match any type.")) }} 14 | {{ render_field(form.license, hint=_("Use shift to select multiple.")) }} 15 | {{ render_field(form.lang) }} 16 | 17 |

    {{ _("Tags and Content Warnings") }}

    18 | 19 | {{ render_field(form.tag, hint=_("Use shift to select multiple.")) }} 20 | {{ render_field(form.flag, hint=_("Use shift to select multiple.")) }} 21 | {{ render_field(form.hide, hint=_("Use shift to select multiple.")) }} 22 | 23 |

    {{ _("Compatibility") }}

    24 | 25 | {{ render_field(form.engine_version) }} 26 | {{ render_field(form.game) }} 27 | 28 |

    {{ _("Sorting") }}

    29 | 30 | {{ render_field(form.sort) }} 31 | {{ render_field(form.order) }} 32 | {{ render_checkbox_field(form.random) }} 33 | 34 | 35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/tests/unit/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | # ContentDB 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # Copyright (C) 2018-2025 rubenwardy 4 | 5 | import user_agents 6 | 7 | from app.utils import make_valid_username 8 | 9 | 10 | def test_make_valid_username(): 11 | assert make_valid_username("rubenwardy") == "rubenwardy" 12 | assert make_valid_username("Test123._-") == "Test123._-" 13 | assert make_valid_username("Foo Bar") == "Foo_Bar" 14 | assert make_valid_username("François") == "Fran_ois" 15 | 16 | 17 | def test_web_is_not_bot(): 18 | assert not user_agents.parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0").is_bot 19 | assert not user_agents.parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 20 | "Chrome/125.0.0.0 Safari/537.36").is_bot 21 | 22 | 23 | def test_luanti_is_not_bot(): 24 | assert not user_agents.parse("Minetest/5.5.1 (Linux/4.14.193+-ab49821 aarch64)").is_bot 25 | assert not user_agents.parse("Luanti/5.12.0 (Linux/4.14.193+-ab49821 aarch64)").is_bot 26 | 27 | 28 | def test_crawlers_are_bots(): 29 | assert user_agents.parse("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, " 30 | "like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; " 31 | "+http://www.google.com/bot.html)").is_bot 32 | --------------------------------------------------------------------------------