├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
18 |
19 |
20 | {% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %}
21 | {{ info.error }}
22 | {% else %}
23 |
24 |
25 | {{ _("Reload the page to check for updates.") }}
26 |
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 |
25 |
26 |
27 |
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 |
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 |
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 |
13 | {% for report in reports %}
14 |
15 |
16 |
17 | {% if report.is_resolved %}
18 |
19 | Closed
20 |
21 | {% else %}
22 |
23 | Open
24 |
25 | {% endif %}
26 | {{ report.title }}
27 | {% if report.user %}
28 | by {{ report.user.display_name }}
29 | {% endif %}
30 |
31 |
32 | {{ report.created_at | timedelta }} ago
33 |
34 |
35 |
36 | {% else %}
37 |
38 | No reports.
39 |
40 | {% endfor %}
41 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------