From f93f3d63ba2f7ff2957bde9ea1657f5f65fb0551 Mon Sep 17 00:00:00 2001
From: Jordan <37647414+linuxgoose@users.noreply.github.com>
Date: Sun, 14 Sep 2025 10:27:04 +0100
Subject: [PATCH] Initial commit from GitHub
---
.build.yml | 21 +
.envrc.example | 24 +
.github/dependabot.yml | 11 +
.github/workflows/check.yml | 49 +
.github/workflows/docs.yml | 50 +
.github/workflows/image.yml | 49 +
.gitignore | 24 +
CHANGELOG.md | 83 +
Dockerfile | 28 +
LICENSE | 661 ++++++++
README.md | 327 ++++
SECURITY.md | 6 +
ansible/.envrc.example | 42 +
ansible/Caddyfile.j2 | 43 +
ansible/ansible.cfg | 3 +
ansible/backup-database.sh | 33 +
ansible/caddy.service.j2 | 22 +
ansible/inventory.yaml | 5 +
ansible/mataroa-backup.service.j2 | 12 +
ansible/mataroa-backup.timer.j2 | 8 +
ansible/mataroa-dailysummary.service.j2 | 12 +
ansible/mataroa-dailysummary.timer.j2 | 8 +
ansible/mataroa-exports.service.j2 | 12 +
ansible/mataroa-exports.timer.j2 | 8 +
ansible/mataroa-notifications.service.j2 | 12 +
ansible/mataroa-notifications.timer.j2 | 8 +
ansible/mataroa.env.j2 | 12 +
ansible/mataroa.service.j2 | 17 +
ansible/playbook.yaml | 201 +++
ansible/vars.yaml | 20 +
docker-compose.yml | 32 +
docs/book.toml | 6 +
docs/src/SUMMARY.md | 13 +
docs/src/coding-conventions.md | 4 +
docs/src/commit-messages.md | 15 +
docs/src/cronjobs.md | 31 +
docs/src/database-backup.md | 45 +
docs/src/dependencies.md | 47 +
docs/src/deployment.md | 66 +
docs/src/file-structure-walkthrough.md | 191 +++
docs/src/introduction.md | 29 +
docs/src/main-repository-readme.md | 1 +
docs/src/runbook.md | 114 ++
docs/src/server-migration.md | 36 +
export_base_epub/container.xml | 6 +
export_base_epub/content.opf | 29 +
export_base_epub/mimetype | 1 +
export_base_epub/toc.ncx | 24 +
export_base_epub/toc.xhtml | 15 +
export_base_hugo/404.html | 19 +
export_base_hugo/baseof.html | 25 +
export_base_hugo/config.toml | 12 +
export_base_hugo/index.html | 33 +
export_base_hugo/list.html | 14 +
export_base_hugo/single.html | 32 +
export_base_hugo/style.css | 151 ++
export_base_hugo/theme.toml | 7 +
export_base_zola/404.html | 15 +
export_base_zola/_index.md | 3 +
export_base_zola/config.toml | 22 +
export_base_zola/index.html | 38 +
export_base_zola/post.html | 26 +
export_base_zola/style.css | 157 ++
main/__init__.py | 0
main/admin.py | 236 +++
main/apps.py | 5 +
main/denylist.py | 370 ++++
main/feeds.py | 161 ++
main/fixtures/dev-data.json | 147 ++
main/forms.py | 75 +
main/management/commands/checkstripe.py | 39 +
main/management/commands/mailexports.py | 116 ++
main/management/commands/mailsummary.py | 141 ++
.../commands/processnotifications.py | 169 ++
main/management/commands/testbulkmail.py | 74 +
main/middleware.py | 119 ++
main/migrations/0001_initial.py | 130 ++
main/migrations/0002_user_blog_title.py | 18 +
main/migrations/0003_post.py | 41 +
main/migrations/0004_auto_20200530_0046.py | 24 +
main/migrations/0005_auto_20200530_1206.py | 16 +
main/migrations/0006_auto_20200531_1619.py | 16 +
main/migrations/0007_user_blog_byline.py | 17 +
main/migrations/0008_auto_20200601_2326.py | 25 +
main/migrations/0009_auto_20200604_2327.py | 17 +
main/migrations/0010_user_cname.py | 17 +
main/migrations/0011_auto_20200606_1903.py | 17 +
main/migrations/0012_auto_20200606_1903.py | 17 +
main/migrations/0013_auto_20200606_2048.py | 25 +
main/migrations/0014_auto_20200607_0017.py | 35 +
main/migrations/0015_auto_20200607_0023.py | 24 +
main/migrations/0016_auto_20200607_0024.py | 25 +
main/migrations/0017_post_published_at.py | 23 +
main/migrations/0018_auto_20200607_2049.py | 23 +
main/migrations/0019_auto_20200607_2131.py | 16 +
main/migrations/0020_auto_20200608_1915.py | 40 +
main/migrations/0021_image_slug.py | 18 +
main/migrations/0022_auto_20200608_2117.py | 17 +
main/migrations/0023_auto_20200608_2141.py | 16 +
main/migrations/0024_auto_20200608_2304.py | 23 +
main/migrations/0025_page.py | 42 +
main/migrations/0026_auto_20200610_1854.py | 23 +
main/migrations/0027_auto_20200610_1904.py | 23 +
main/migrations/0028_auto_20200610_2248.py | 20 +
main/migrations/0029_user_footer_note.py | 17 +
main/migrations/0030_auto_20200613_1726.py | 17 +
main/migrations/0031_auto_20200620_1344.py | 47 +
main/migrations/0032_auto_20200620_1431.py | 32 +
main/migrations/0033_auto_20200626_1947.py | 27 +
main/migrations/0034_analytic.py | 34 +
main/migrations/0035_auto_20200812_2020.py | 47 +
main/migrations/0036_auto_20200812_2102.py | 22 +
main/migrations/0037_auto_20200812_2126.py | 24 +
main/migrations/0038_auto_20200812_2152.py | 16 +
main/migrations/0039_auto_20200816_1543.py | 24 +
...postnotification_postnotificationrecord.py | 66 +
main/migrations/0041_auto_20200820_2107.py | 16 +
main/migrations/0042_auto_20200821_1342.py | 19 +
main/migrations/0043_user_notifications_on.py | 20 +
.../0044_postnotificationrecord_post.py | 20 +
main/migrations/0045_auto_20200821_1659.py | 16 +
main/migrations/0046_auto_20200821_1820.py | 18 +
main/migrations/0047_auto_20200830_1057.py | 20 +
main/migrations/0048_auto_20201218_1351.py | 20 +
main/migrations/0049_user_redirect_domain.py | 25 +
main/migrations/0050_auto_20210101_1509.py | 25 +
main/migrations/0051_auto_20210111_2111.py | 29 +
main/migrations/0052_auto_20210124_1932.py | 30 +
.../migrations/0053_notification_is_active.py | 17 +
main/migrations/0054_auto_20210312_1643.py | 25 +
main/migrations/0055_user_theme_zialucia.py | 21 +
main/migrations/0056_auto_20210317_2313.py | 25 +
main/migrations/0057_auto_20210317_2321.py | 23 +
.../0058_remove_analytic_referer.py | 16 +
main/migrations/0059_auto_20210409_1320.py | 68 +
main/migrations/0060_auto_20210429_1506.py | 35 +
main/migrations/0061_auto_20210503_0035.py | 27 +
main/migrations/0062_auto_20210516_2310.py | 39 +
main/migrations/0063_exportrecord.py | 40 +
.../0064_user_export_unsubscribe_key.py | 19 +
...065_remove_uuid_null_export_unsubscribe.py | 22 +
.../0066_add_uuid_field_export_unsubscribe.py | 19 +
.../0067_rename_analytic_analyticpost.py | 16 +
main/migrations/0068_analyticpage.py | 40 +
.../0069_alter_analyticpage_path.py | 17 +
.../0070_notificationrecord_is_canceled.py | 17 +
main/migrations/0071_user_monero_address.py | 17 +
main/migrations/0072_alter_user_username.py | 28 +
main/migrations/0073_user_api_key.py | 21 +
.../0074_populate_api_key_values.py | 22 +
main/migrations/0075_remove_api_key_null.py | 21 +
.../migrations/0076_alter_user_footer_note.py | 23 +
main/migrations/0077_comment_is_approved.py | 17 +
.../0078_alter_user_theme_zialucia.py | 21 +
...eme_sansserif_alter_user_theme_zialucia.py | 30 +
.../0080_alter_user_theme_sansserif.py | 21 +
...ent_is_author_alter_comment_is_approved.py | 24 +
main/migrations/0082_snapshot.py | 41 +
main/migrations/0083_user_post_backups_on.py | 21 +
.../0084_alter_user_post_backups_on.py | 21 +
.../migrations/0085_alter_user_footer_note.py | 22 +
.../migrations/0086_alter_user_blog_byline.py | 19 +
main/migrations/0087_user_is_approved.py | 17 +
main/migrations/0088_alter_page_is_hidden.py | 20 +
.../0089_user_stripe_subscription_id.py | 17 +
main/migrations/0090_onboard.py | 37 +
main/migrations/0091_onboard_created_at.py | 21 +
main/migrations/0092_alter_onboard_user.py | 24 +
..._onboard_problems_alter_onboard_quality.py | 22 +
main/migrations/0094_onboard_code.py | 19 +
main/migrations/0095_auto_20231109_2111.py | 22 +
main/migrations/0096_auto_20231109_2111.py | 19 +
main/migrations/0097_user_subscribe_note.py | 22 +
.../0098_alter_user_notifications_on.py | 21 +
.../0099_alter_user_subscribe_note.py | 23 +
main/migrations/0100_onboard_seo.py | 18 +
main/migrations/0101_post_broadcasted_at.py | 17 +
...2_remove_notificationrecord_is_canceled.py | 16 +
.../0103_alter_post_broadcasted_at.py | 17 +
...problems_alter_onboard_quality_and_more.py | 27 +
...content_alter_user_footer_note_and_more.py | 29 +
...av_user_show_posts_on_homepage_and_more.py | 29 +
.../0107_alter_user_show_posts_on_homepage.py | 18 +
main/migrations/0108_user_noindex_on.py | 18 +
main/migrations/0109_user_posts_page_title.py | 18 +
main/migrations/0110_user_robots_txt.py | 18 +
...r_reading_time_on_alter_user_robots_txt.py | 23 +
main/migrations/0112_rsl_settings.py | 27 +
...name_rsl_settings_reallysimplelicensing.py | 17 +
...14_alter_reallysimplelicensing_show_rss.py | 18 +
...115_alter_reallysimplelicensing_license.py | 18 +
.../0116_alter_reallysimplelicensing_user.py | 20 +
..._reallysimplelicensing_license_and_more.py | 38 +
main/migrations/__init__.py | 0
main/models.py | 510 ++++++
main/sitemaps.py | 56 +
main/static/favicon.png | Bin 0 -> 411 bytes
main/static/logo.svg | 16 +
main/templates/400.html | 13 +
main/templates/403.html | 13 +
main/templates/404.html | 12 +
main/templates/500.html | 13 +
main/templates/assets/drag-and-drop-upload.js | 82 +
main/templates/assets/make-draft-button.js | 35 +
main/templates/assets/save-snapshot.js | 58 +
main/templates/assets/style.css | 949 +++++++++++
main/templates/main/analytic_detail.html | 60 +
main/templates/main/analytic_list.html | 33 +
main/templates/main/api_docs.html | 203 +++
main/templates/main/api_key_reset.html | 21 +
main/templates/main/billing_card.html | 103 ++
.../main/billing_card_confirm_delete.html | 24 +
main/templates/main/billing_index.html | 163 ++
main/templates/main/billing_subscribe.html | 119 ++
.../main/billing_subscription_cancel.html | 14 +
main/templates/main/blog_import.html | 29 +
main/templates/main/blog_index.html | 97 ++
main/templates/main/blog_posts.html | 85 +
main/templates/main/comment_approve.html | 23 +
.../main/comment_confirm_delete.html | 33 +
main/templates/main/comment_form.html | 64 +
main/templates/main/comment_list.html | 35 +
main/templates/main/comparisons.html | 164 ++
main/templates/main/dashboard.html | 75 +
main/templates/main/export_index.html | 137 ++
main/templates/main/export_print.html | 53 +
.../main/export_unsubscribe_success.html | 26 +
main/templates/main/guides_comments.html | 50 +
main/templates/main/guides_customdomain.html | 42 +
main/templates/main/guides_images.html | 46 +
main/templates/main/guides_markdown.html | 106 ++
main/templates/main/image_confirm_delete.html | 13 +
main/templates/main/image_detail.html | 43 +
main/templates/main/image_form.html | 21 +
main/templates/main/image_list.html | 37 +
main/templates/main/landing.html | 241 +++
main/templates/main/layout.html | 88 +
main/templates/main/methodology.html | 346 ++++
main/templates/main/moderation_activity.html | 183 ++
main/templates/main/moderation_cohorts.html | 28 +
main/templates/main/moderation_images.html | 46 +
main/templates/main/moderation_index.html | 22 +
main/templates/main/moderation_posts.html | 58 +
main/templates/main/moderation_stats.html | 66 +
main/templates/main/moderation_summary.html | 107 ++
main/templates/main/moderation_user_list.html | 265 +++
.../main/moderation_user_single.html | 189 +++
main/templates/main/notification.html | 25 +
main/templates/main/notification_list.html | 28 +
.../main/notification_unsubscribe.html | 25 +
.../notification_unsubscribe_success.html | 27 +
.../notificationrecord_confirm_delete.html | 18 +
.../main/notificationrecord_list.html | 49 +
main/templates/main/page_confirm_delete.html | 13 +
main/templates/main/page_detail.html | 51 +
main/templates/main/page_form.html | 70 +
main/templates/main/page_list.html | 26 +
main/templates/main/post_confirm_delete.html | 13 +
main/templates/main/post_detail.html | 196 +++
main/templates/main/post_form.html | 90 +
main/templates/main/post_list.html | 36 +
main/templates/main/rsl_update.html | 29 +
.../main/snapshot_confirm_delete.html | 13 +
main/templates/main/snapshot_detail.html | 37 +
main/templates/main/snapshot_list.html | 25 +
main/templates/main/transparency.html | 117 ++
main/templates/main/user_confirm_delete.html | 16 +
main/templates/main/user_create_step_one.html | 54 +
main/templates/main/user_create_step_two.html | 71 +
main/templates/main/user_update.html | 20 +
main/templates/main/webring.html | 34 +
main/templates/partials/footer.html | 5 +
main/templates/partials/footer_blog.html | 7 +
main/templates/partials/rsl_license_head.html | 7 +
main/templates/partials/webring.html | 20 +
main/templates/registration/logged_out.html | 10 +
main/templates/registration/login.html | 56 +
.../registration/password_change_done.html | 10 +
.../registration/password_change_form.html | 15 +
.../registration/password_reset_complete.html | 10 +
.../registration/password_reset_confirm.html | 14 +
.../registration/password_reset_done.html | 10 +
.../registration/password_reset_email.html | 14 +
.../registration/password_reset_form.html | 15 +
main/tests/__init__.py | 0
main/tests/test_analytics.py | 296 ++++
main/tests/test_api.py | 745 ++++++++
main/tests/test_billing.py | 294 ++++
main/tests/test_blog.py | 420 +++++
main/tests/test_comments.py | 563 +++++++
main/tests/test_feeds.py | 129 ++
main/tests/test_images.py | 244 +++
main/tests/test_management.py | 200 +++
main/tests/test_pages.py | 297 ++++
main/tests/test_posts.py | 432 +++++
main/tests/test_sitemap.py | 169 ++
main/tests/test_snapshots.py | 109 ++
main/tests/test_static.py | 30 +
main/tests/test_users.py | 291 ++++
main/tests/test_webring.py | 117 ++
main/tests/testdata/lorem.md | 5 +
main/tests/testdata/vulf.jpeg | Bin 0 -> 43845 bytes
main/urls.py | 282 ++++
main/util.py | 220 +++
main/validators.py | 20 +
main/views/__init__.py | 0
main/views/api.py | 187 ++
main/views/billing.py | 534 ++++++
main/views/export.py | 457 +++++
main/views/general.py | 1501 +++++++++++++++++
main/views/moderation.py | 781 +++++++++
manage.py | 23 +
mataroa/__init__.py | 0
mataroa/asgi.py | 16 +
mataroa/settings.py | 248 +++
mataroa/urls.py | 23 +
mataroa/wsgi.py | 16 +
postgresql/Makefile | 18 +
pyproject.toml | 41 +
uv.lock | 573 +++++++
320 files changed, 23498 insertions(+)
create mode 100644 .build.yml
create mode 100644 .envrc.example
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/check.yml
create mode 100644 .github/workflows/docs.yml
create mode 100644 .github/workflows/image.yml
create mode 100644 .gitignore
create mode 100644 CHANGELOG.md
create mode 100644 Dockerfile
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 SECURITY.md
create mode 100644 ansible/.envrc.example
create mode 100644 ansible/Caddyfile.j2
create mode 100644 ansible/ansible.cfg
create mode 100755 ansible/backup-database.sh
create mode 100644 ansible/caddy.service.j2
create mode 100644 ansible/inventory.yaml
create mode 100644 ansible/mataroa-backup.service.j2
create mode 100644 ansible/mataroa-backup.timer.j2
create mode 100644 ansible/mataroa-dailysummary.service.j2
create mode 100644 ansible/mataroa-dailysummary.timer.j2
create mode 100644 ansible/mataroa-exports.service.j2
create mode 100644 ansible/mataroa-exports.timer.j2
create mode 100644 ansible/mataroa-notifications.service.j2
create mode 100644 ansible/mataroa-notifications.timer.j2
create mode 100644 ansible/mataroa.env.j2
create mode 100644 ansible/mataroa.service.j2
create mode 100644 ansible/playbook.yaml
create mode 100644 ansible/vars.yaml
create mode 100644 docker-compose.yml
create mode 100644 docs/book.toml
create mode 100644 docs/src/SUMMARY.md
create mode 100644 docs/src/coding-conventions.md
create mode 100644 docs/src/commit-messages.md
create mode 100644 docs/src/cronjobs.md
create mode 100644 docs/src/database-backup.md
create mode 100644 docs/src/dependencies.md
create mode 100644 docs/src/deployment.md
create mode 100644 docs/src/file-structure-walkthrough.md
create mode 100644 docs/src/introduction.md
create mode 120000 docs/src/main-repository-readme.md
create mode 100644 docs/src/runbook.md
create mode 100644 docs/src/server-migration.md
create mode 100644 export_base_epub/container.xml
create mode 100644 export_base_epub/content.opf
create mode 100644 export_base_epub/mimetype
create mode 100644 export_base_epub/toc.ncx
create mode 100644 export_base_epub/toc.xhtml
create mode 100644 export_base_hugo/404.html
create mode 100644 export_base_hugo/baseof.html
create mode 100644 export_base_hugo/config.toml
create mode 100644 export_base_hugo/index.html
create mode 100644 export_base_hugo/list.html
create mode 100644 export_base_hugo/single.html
create mode 100644 export_base_hugo/style.css
create mode 100644 export_base_hugo/theme.toml
create mode 100644 export_base_zola/404.html
create mode 100644 export_base_zola/_index.md
create mode 100644 export_base_zola/config.toml
create mode 100644 export_base_zola/index.html
create mode 100644 export_base_zola/post.html
create mode 100644 export_base_zola/style.css
create mode 100644 main/__init__.py
create mode 100644 main/admin.py
create mode 100644 main/apps.py
create mode 100644 main/denylist.py
create mode 100644 main/feeds.py
create mode 100644 main/fixtures/dev-data.json
create mode 100644 main/forms.py
create mode 100644 main/management/commands/checkstripe.py
create mode 100644 main/management/commands/mailexports.py
create mode 100644 main/management/commands/mailsummary.py
create mode 100644 main/management/commands/processnotifications.py
create mode 100644 main/management/commands/testbulkmail.py
create mode 100644 main/middleware.py
create mode 100644 main/migrations/0001_initial.py
create mode 100644 main/migrations/0002_user_blog_title.py
create mode 100644 main/migrations/0003_post.py
create mode 100644 main/migrations/0004_auto_20200530_0046.py
create mode 100644 main/migrations/0005_auto_20200530_1206.py
create mode 100644 main/migrations/0006_auto_20200531_1619.py
create mode 100644 main/migrations/0007_user_blog_byline.py
create mode 100644 main/migrations/0008_auto_20200601_2326.py
create mode 100644 main/migrations/0009_auto_20200604_2327.py
create mode 100644 main/migrations/0010_user_cname.py
create mode 100644 main/migrations/0011_auto_20200606_1903.py
create mode 100644 main/migrations/0012_auto_20200606_1903.py
create mode 100644 main/migrations/0013_auto_20200606_2048.py
create mode 100644 main/migrations/0014_auto_20200607_0017.py
create mode 100644 main/migrations/0015_auto_20200607_0023.py
create mode 100644 main/migrations/0016_auto_20200607_0024.py
create mode 100644 main/migrations/0017_post_published_at.py
create mode 100644 main/migrations/0018_auto_20200607_2049.py
create mode 100644 main/migrations/0019_auto_20200607_2131.py
create mode 100644 main/migrations/0020_auto_20200608_1915.py
create mode 100644 main/migrations/0021_image_slug.py
create mode 100644 main/migrations/0022_auto_20200608_2117.py
create mode 100644 main/migrations/0023_auto_20200608_2141.py
create mode 100644 main/migrations/0024_auto_20200608_2304.py
create mode 100644 main/migrations/0025_page.py
create mode 100644 main/migrations/0026_auto_20200610_1854.py
create mode 100644 main/migrations/0027_auto_20200610_1904.py
create mode 100644 main/migrations/0028_auto_20200610_2248.py
create mode 100644 main/migrations/0029_user_footer_note.py
create mode 100644 main/migrations/0030_auto_20200613_1726.py
create mode 100644 main/migrations/0031_auto_20200620_1344.py
create mode 100644 main/migrations/0032_auto_20200620_1431.py
create mode 100644 main/migrations/0033_auto_20200626_1947.py
create mode 100644 main/migrations/0034_analytic.py
create mode 100644 main/migrations/0035_auto_20200812_2020.py
create mode 100644 main/migrations/0036_auto_20200812_2102.py
create mode 100644 main/migrations/0037_auto_20200812_2126.py
create mode 100644 main/migrations/0038_auto_20200812_2152.py
create mode 100644 main/migrations/0039_auto_20200816_1543.py
create mode 100644 main/migrations/0040_postnotification_postnotificationrecord.py
create mode 100644 main/migrations/0041_auto_20200820_2107.py
create mode 100644 main/migrations/0042_auto_20200821_1342.py
create mode 100644 main/migrations/0043_user_notifications_on.py
create mode 100644 main/migrations/0044_postnotificationrecord_post.py
create mode 100644 main/migrations/0045_auto_20200821_1659.py
create mode 100644 main/migrations/0046_auto_20200821_1820.py
create mode 100644 main/migrations/0047_auto_20200830_1057.py
create mode 100644 main/migrations/0048_auto_20201218_1351.py
create mode 100644 main/migrations/0049_user_redirect_domain.py
create mode 100644 main/migrations/0050_auto_20210101_1509.py
create mode 100644 main/migrations/0051_auto_20210111_2111.py
create mode 100644 main/migrations/0052_auto_20210124_1932.py
create mode 100644 main/migrations/0053_notification_is_active.py
create mode 100644 main/migrations/0054_auto_20210312_1643.py
create mode 100644 main/migrations/0055_user_theme_zialucia.py
create mode 100644 main/migrations/0056_auto_20210317_2313.py
create mode 100644 main/migrations/0057_auto_20210317_2321.py
create mode 100644 main/migrations/0058_remove_analytic_referer.py
create mode 100644 main/migrations/0059_auto_20210409_1320.py
create mode 100644 main/migrations/0060_auto_20210429_1506.py
create mode 100644 main/migrations/0061_auto_20210503_0035.py
create mode 100644 main/migrations/0062_auto_20210516_2310.py
create mode 100644 main/migrations/0063_exportrecord.py
create mode 100644 main/migrations/0064_user_export_unsubscribe_key.py
create mode 100644 main/migrations/0065_remove_uuid_null_export_unsubscribe.py
create mode 100644 main/migrations/0066_add_uuid_field_export_unsubscribe.py
create mode 100644 main/migrations/0067_rename_analytic_analyticpost.py
create mode 100644 main/migrations/0068_analyticpage.py
create mode 100644 main/migrations/0069_alter_analyticpage_path.py
create mode 100644 main/migrations/0070_notificationrecord_is_canceled.py
create mode 100644 main/migrations/0071_user_monero_address.py
create mode 100644 main/migrations/0072_alter_user_username.py
create mode 100644 main/migrations/0073_user_api_key.py
create mode 100644 main/migrations/0074_populate_api_key_values.py
create mode 100644 main/migrations/0075_remove_api_key_null.py
create mode 100644 main/migrations/0076_alter_user_footer_note.py
create mode 100644 main/migrations/0077_comment_is_approved.py
create mode 100644 main/migrations/0078_alter_user_theme_zialucia.py
create mode 100644 main/migrations/0079_user_theme_sansserif_alter_user_theme_zialucia.py
create mode 100644 main/migrations/0080_alter_user_theme_sansserif.py
create mode 100644 main/migrations/0081_comment_is_author_alter_comment_is_approved.py
create mode 100644 main/migrations/0082_snapshot.py
create mode 100644 main/migrations/0083_user_post_backups_on.py
create mode 100644 main/migrations/0084_alter_user_post_backups_on.py
create mode 100644 main/migrations/0085_alter_user_footer_note.py
create mode 100644 main/migrations/0086_alter_user_blog_byline.py
create mode 100644 main/migrations/0087_user_is_approved.py
create mode 100644 main/migrations/0088_alter_page_is_hidden.py
create mode 100644 main/migrations/0089_user_stripe_subscription_id.py
create mode 100644 main/migrations/0090_onboard.py
create mode 100644 main/migrations/0091_onboard_created_at.py
create mode 100644 main/migrations/0092_alter_onboard_user.py
create mode 100644 main/migrations/0093_alter_onboard_problems_alter_onboard_quality.py
create mode 100644 main/migrations/0094_onboard_code.py
create mode 100644 main/migrations/0095_auto_20231109_2111.py
create mode 100644 main/migrations/0096_auto_20231109_2111.py
create mode 100644 main/migrations/0097_user_subscribe_note.py
create mode 100644 main/migrations/0098_alter_user_notifications_on.py
create mode 100644 main/migrations/0099_alter_user_subscribe_note.py
create mode 100644 main/migrations/0100_onboard_seo.py
create mode 100644 main/migrations/0101_post_broadcasted_at.py
create mode 100644 main/migrations/0102_remove_notificationrecord_is_canceled.py
create mode 100644 main/migrations/0103_alter_post_broadcasted_at.py
create mode 100644 main/migrations/0104_alter_onboard_problems_alter_onboard_quality_and_more.py
create mode 100644 main/migrations/0105_user_blog_index_content_alter_user_footer_note_and_more.py
create mode 100644 main/migrations/0106_user_show_posts_in_nav_user_show_posts_on_homepage_and_more.py
create mode 100644 main/migrations/0107_alter_user_show_posts_on_homepage.py
create mode 100644 main/migrations/0108_user_noindex_on.py
create mode 100644 main/migrations/0109_user_posts_page_title.py
create mode 100644 main/migrations/0110_user_robots_txt.py
create mode 100644 main/migrations/0111_user_reading_time_on_alter_user_robots_txt.py
create mode 100644 main/migrations/0112_rsl_settings.py
create mode 100644 main/migrations/0113_rename_rsl_settings_reallysimplelicensing.py
create mode 100644 main/migrations/0114_alter_reallysimplelicensing_show_rss.py
create mode 100644 main/migrations/0115_alter_reallysimplelicensing_license.py
create mode 100644 main/migrations/0116_alter_reallysimplelicensing_user.py
create mode 100644 main/migrations/0117_alter_reallysimplelicensing_license_and_more.py
create mode 100644 main/migrations/__init__.py
create mode 100644 main/models.py
create mode 100644 main/sitemaps.py
create mode 100644 main/static/favicon.png
create mode 100644 main/static/logo.svg
create mode 100644 main/templates/400.html
create mode 100644 main/templates/403.html
create mode 100644 main/templates/404.html
create mode 100644 main/templates/500.html
create mode 100644 main/templates/assets/drag-and-drop-upload.js
create mode 100644 main/templates/assets/make-draft-button.js
create mode 100644 main/templates/assets/save-snapshot.js
create mode 100644 main/templates/assets/style.css
create mode 100644 main/templates/main/analytic_detail.html
create mode 100644 main/templates/main/analytic_list.html
create mode 100644 main/templates/main/api_docs.html
create mode 100644 main/templates/main/api_key_reset.html
create mode 100644 main/templates/main/billing_card.html
create mode 100644 main/templates/main/billing_card_confirm_delete.html
create mode 100644 main/templates/main/billing_index.html
create mode 100644 main/templates/main/billing_subscribe.html
create mode 100644 main/templates/main/billing_subscription_cancel.html
create mode 100644 main/templates/main/blog_import.html
create mode 100644 main/templates/main/blog_index.html
create mode 100644 main/templates/main/blog_posts.html
create mode 100644 main/templates/main/comment_approve.html
create mode 100644 main/templates/main/comment_confirm_delete.html
create mode 100644 main/templates/main/comment_form.html
create mode 100644 main/templates/main/comment_list.html
create mode 100644 main/templates/main/comparisons.html
create mode 100644 main/templates/main/dashboard.html
create mode 100644 main/templates/main/export_index.html
create mode 100644 main/templates/main/export_print.html
create mode 100644 main/templates/main/export_unsubscribe_success.html
create mode 100644 main/templates/main/guides_comments.html
create mode 100644 main/templates/main/guides_customdomain.html
create mode 100644 main/templates/main/guides_images.html
create mode 100644 main/templates/main/guides_markdown.html
create mode 100644 main/templates/main/image_confirm_delete.html
create mode 100644 main/templates/main/image_detail.html
create mode 100644 main/templates/main/image_form.html
create mode 100644 main/templates/main/image_list.html
create mode 100644 main/templates/main/landing.html
create mode 100644 main/templates/main/layout.html
create mode 100644 main/templates/main/methodology.html
create mode 100644 main/templates/main/moderation_activity.html
create mode 100644 main/templates/main/moderation_cohorts.html
create mode 100644 main/templates/main/moderation_images.html
create mode 100644 main/templates/main/moderation_index.html
create mode 100644 main/templates/main/moderation_posts.html
create mode 100644 main/templates/main/moderation_stats.html
create mode 100644 main/templates/main/moderation_summary.html
create mode 100644 main/templates/main/moderation_user_list.html
create mode 100644 main/templates/main/moderation_user_single.html
create mode 100644 main/templates/main/notification.html
create mode 100644 main/templates/main/notification_list.html
create mode 100644 main/templates/main/notification_unsubscribe.html
create mode 100644 main/templates/main/notification_unsubscribe_success.html
create mode 100644 main/templates/main/notificationrecord_confirm_delete.html
create mode 100644 main/templates/main/notificationrecord_list.html
create mode 100644 main/templates/main/page_confirm_delete.html
create mode 100644 main/templates/main/page_detail.html
create mode 100644 main/templates/main/page_form.html
create mode 100644 main/templates/main/page_list.html
create mode 100644 main/templates/main/post_confirm_delete.html
create mode 100644 main/templates/main/post_detail.html
create mode 100644 main/templates/main/post_form.html
create mode 100644 main/templates/main/post_list.html
create mode 100644 main/templates/main/rsl_update.html
create mode 100644 main/templates/main/snapshot_confirm_delete.html
create mode 100644 main/templates/main/snapshot_detail.html
create mode 100644 main/templates/main/snapshot_list.html
create mode 100644 main/templates/main/transparency.html
create mode 100644 main/templates/main/user_confirm_delete.html
create mode 100644 main/templates/main/user_create_step_one.html
create mode 100644 main/templates/main/user_create_step_two.html
create mode 100644 main/templates/main/user_update.html
create mode 100644 main/templates/main/webring.html
create mode 100644 main/templates/partials/footer.html
create mode 100644 main/templates/partials/footer_blog.html
create mode 100644 main/templates/partials/rsl_license_head.html
create mode 100644 main/templates/partials/webring.html
create mode 100644 main/templates/registration/logged_out.html
create mode 100644 main/templates/registration/login.html
create mode 100644 main/templates/registration/password_change_done.html
create mode 100644 main/templates/registration/password_change_form.html
create mode 100644 main/templates/registration/password_reset_complete.html
create mode 100644 main/templates/registration/password_reset_confirm.html
create mode 100644 main/templates/registration/password_reset_done.html
create mode 100644 main/templates/registration/password_reset_email.html
create mode 100644 main/templates/registration/password_reset_form.html
create mode 100644 main/tests/__init__.py
create mode 100644 main/tests/test_analytics.py
create mode 100644 main/tests/test_api.py
create mode 100644 main/tests/test_billing.py
create mode 100644 main/tests/test_blog.py
create mode 100644 main/tests/test_comments.py
create mode 100644 main/tests/test_feeds.py
create mode 100644 main/tests/test_images.py
create mode 100644 main/tests/test_management.py
create mode 100644 main/tests/test_pages.py
create mode 100644 main/tests/test_posts.py
create mode 100644 main/tests/test_sitemap.py
create mode 100644 main/tests/test_snapshots.py
create mode 100644 main/tests/test_static.py
create mode 100644 main/tests/test_users.py
create mode 100644 main/tests/test_webring.py
create mode 100644 main/tests/testdata/lorem.md
create mode 100644 main/tests/testdata/vulf.jpeg
create mode 100644 main/urls.py
create mode 100644 main/util.py
create mode 100644 main/validators.py
create mode 100644 main/views/__init__.py
create mode 100644 main/views/api.py
create mode 100644 main/views/billing.py
create mode 100644 main/views/export.py
create mode 100644 main/views/general.py
create mode 100644 main/views/moderation.py
create mode 100755 manage.py
create mode 100644 mataroa/__init__.py
create mode 100644 mataroa/asgi.py
create mode 100644 mataroa/settings.py
create mode 100644 mataroa/urls.py
create mode 100644 mataroa/wsgi.py
create mode 100644 postgresql/Makefile
create mode 100644 pyproject.toml
create mode 100644 uv.lock
diff --git a/.build.yml b/.build.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fe6e108087f114ae6270b50a585d5bb171522333
--- /dev/null
+++ b/.build.yml
@@ -0,0 +1,21 @@
+image: ubuntu/24.04
+packages:
+ - curl
+ - postgresql
+tasks:
+ - test: |
+ curl -LsSf https://astral.sh/uv/0.8.8/install.sh | sh
+ ~/.local/bin/uv run python -V
+ sudo -u postgres psql -U postgres -d postgres -c "ALTER USER postgres WITH PASSWORD 'postgres';"
+ cd mataroa/
+ export DEBUG=1
+ export SECRET_KEY='thisisthesecretkey'
+ export DATABASE_URL='postgres://postgres:postgres@localhost:5432/postgres'
+ ~/.local/bin/uv run manage.py collectstatic --noinput
+ ~/.local/bin/uv run manage.py test
+ - lint: |
+ curl -LsSf https://astral.sh/uv/0.8.8/install.sh | sh
+ ~/.local/bin/uv run python -V
+ cd mataroa/
+ ~/.local/bin/uv run ruff check
+ ~/.local/bin/uv run djade main/templates/**/*.html
diff --git a/.envrc.example b/.envrc.example
new file mode 100644
index 0000000000000000000000000000000000000000..d3b937f2cd9b9e7100b60a6fe488c3b371dc3dd8
--- /dev/null
+++ b/.envrc.example
@@ -0,0 +1,24 @@
+# Exceptions and tracebacks on errors
+# 1: show
+# 0: don't show
+export DEBUG=1
+
+# Stop real emails and turn https off
+# 1: stop and off
+# 0: do not stop and on
+export LOCALDEV=1
+
+# Session cookies secret
+export SECRET_KEY=some-secret-key
+
+# Database connection
+export DATABASE_URL=postgres://mataroa:xxx@localhost:5432/mataroa
+
+# SMTP credentials
+export EMAIL_HOST_USER=
+export EMAIL_HOST_PASSWORD=
+
+# Stripe payments details
+export STRIPE_API_KEY=
+export STRIPE_PUBLIC_KEY=
+export STRIPE_PRICE_ID=
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5990d9c64c141eb290490adc52f4cc3da248232b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e9ffc3e1aaddf3618b995228ee28e4c9ff64704b
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,49 @@
+name: Check
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+
+ services:
+ postgres:
+ image: postgres
+ env:
+ POSTGRES_PASSWORD: postgres
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.13'
+
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/0.8.8/install.sh | sh
+
+ - name: Run Python linting
+ run: uv run ruff check
+
+ - name: Run HTML linting
+ run: uv run djade main/templates/**/*.html
+
+ - name: Run Tests
+ run: |
+ uv run manage.py collectstatic --no-input
+ uv run manage.py test
+ env:
+ DEBUG: '1'
+ SECRET_KEY: 'thisisthesecretkey'
+ DATABASE_URL: 'postgres://postgres:postgres@localhost:5432/postgres'
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a62c66b7c3b25d8432de9b9d5859a3cf56ed0d48
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,50 @@
+name: Docs
+
+on:
+ push:
+ branches: [ main ]
+ workflow_dispatch:
+
+# Set permissions to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+ env:
+ MDBOOK_VERSION: 0.4.40
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install mdBook
+ run: |
+ # https://github.com/rust-lang/mdBook
+ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh
+ rustup update
+ cargo install --version ${MDBOOK_VERSION} mdbook
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v5
+ - name: Build with mdBook
+ run: cd docs && mdbook build
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./docs/book
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-24.04
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5352b4a02578b116f8f580f0665ddb56b0bdfa15
--- /dev/null
+++ b/.github/workflows/image.yml
@@ -0,0 +1,49 @@
+name: Image
+
+on:
+ release:
+ types: [ published ]
+ workflow_dispatch:
+
+# Set permissions to allow deployment to ghcr.io
+permissions:
+ packages: write
+ contents: read
+
+# Prevent concurrent builds
+concurrency:
+ group: image-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ push:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository }}
+ tags: |
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+
+ - uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ab9177c1fea654f57df493c7fae2c6b6b779e73c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# python
+*.pyc
+
+# environment
+.envrc
+.venv/
+postgres-data/
+
+# generated static
+/static/
+
+# testing
+.coverage
+htmlcov/
+
+# docker
+docker-postgres-data/
+docker-compose.override.yml
+
+# mdbook docs
+/docs/book/
+.env
+db.sqlite3
+debug.log
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..896895cd4303ad5e200e95e64fcbe94009c5a1ef
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,83 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [1.3.2](https://github.com/linuxgoose/mataroa/releases/tag/1.3.2)
+* add blog index content by @linuxgoose in https://github.com/linuxgoose/mataroa/pull/23
+* [add custom robots.txt blog setting](https://github.com/linuxgoose/mataroa/commit/47bd06b3a71d75ddf73a91958b2e3101c675633e) by @linuxgoose
+* [add custom posts page title](https://github.com/linuxgoose/mataroa/commit/3ac28fdb08b59310c6fcc313a6af511320a66f50)
+* [add more feed urls](https://github.com/linuxgoose/mataroa/commit/b57c9d0b06642b4983258ffd2bdf57cd18d49613) by @linuxgoose
+* [add noindex support - meta tag](https://github.com/linuxgoose/mataroa/commit/dcbd6e2360068953bcd72910c76574b4a63a35a0) by @linuxgoose
+* [add ability to set post_backups_on in blog settings](https://github.com/linuxgoose/mataroa/commit/ac590b7092b9786b1d7e4d1cb6aa6a7c88937237) by @linuxgoose
+* [add blog index content](https://github.com/linuxgoose/mataroa/commit/f480264f048edbf0b9164388e3a77de95c48fc55) by @linuxgoose
+
+## [1.3.1](https://github.com/linuxgoose/mataroa/releases/tag/1.3.1)
+* Add LaTeX Support with l2m4m dependency & mathml
+
+## [1.3.0](https://github.com/mataroablog/mataroa/compare/v1.2...v1.3)
+
+### Important changes
+
+* Rebuild content moderation dashboard with:
+ * pagination
+ * filters
+ * sort by
+ * day summary
+ * images overview
+ * global stats
+ * daily admin summary email
+* Switch to astral uv
+* Add hard check for image uploading limit
+* Change sign up text
+* Remove robot checks from sign up form
+* Upgrade to Django 5.2
+* Add docker image auto-push to ghcr.io
+
+### Bug fixes
+
+* Improve dark mode colours for better readability
+* Fix ansible not auto-enabling systemd timers
+
+## [1.2.0](https://github.com/mataroablog/mataroa/compare/v1.1...v1.2) - 2025-02-06
+
+### Important changes
+
+* Change project license from MIT to AGPL-3.0-only
+* Enable customisation of subscribe note on footer
+* Introduce ansible configuration for deployment
+* Switch jobs from cron to systemd timers
+* Replace uWSGI with Gunicorn
+* Replace black/flake8/isort with ruff
+* Refactor newsletter processing into more robust and simpler workflow
+* Setup docs using mdbook
+* Improve docker local development setup
+* Add guide for custom domains
+* Simplify Zola and Hugo base CSS styles
+* Add themed error pages
+* Upgrade to Django 5.1
+* Limit RSS to last 10 posts
+
+### Bugfixes
+
+* Fix Zola v0.19 RSS feed configuration
+
+## [1.1.0](https://github.com/mataroablog/mataroa/compare/v1.0...v1.1) - 2023-12-05
+
+### Important changes
+
+* Rewrite moderation dashboard
+* Rewrite Stripe integration with latest APIs
+* Create new signup workflow
+* Lower image size limit to 1MB
+* Upgrade to Django 5.0
+
+## [1.0.0](https://github.com/mataroablog/mataroa/compare/5ff277da71fb653631ea38407cd6154e831be540...v1.0) - 2023-09-06
+
+This is an initial numbered release after 3+ years of development.
+
+* Core blogging functionalities
+* Export functionalities
+* Email newsletter
+* Custom domains
+* Backend-based analytics
+* API
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..659dfcb7b7ad53290188f124ad05627178d3081e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+FROM python:3.13-slim-bookworm
+
+ENV PYTHONUNBUFFERED=1 \
+ PYTHONFAULTHANDLER=1 \
+ VIRTUAL_ENV=/opt/venv \
+ PATH="/opt/venv/bin:$PATH"
+
+RUN pip install uv
+
+# Create the virtual environment directory
+RUN python -m venv $VIRTUAL_ENV
+
+WORKDIR /code
+
+COPY pyproject.toml uv.lock /code/
+
+RUN uv sync --all-groups --project .
+
+RUN rm -rf /code/pyproject.toml /code/uv.lock
+
+# mount local code over /code, but /opt/venv remains untouched
+COPY . /code/
+
+# Expose port 8000 for the Django development server
+EXPOSE 8000
+
+# Command to run the Django development server (can be overridden by docker-compose.yml)
+CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..be3f7b28e564e7dd05eaf59d64adba1a4065ac0e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f6ef6f574da6cd225763984dc18961427725ff40
--- /dev/null
+++ b/README.md
@@ -0,0 +1,327 @@
+# mataroa
+
+Naked blogging platform.
+
+## Community
+
+We have a mailing list at
+[~sirodoht/mataroa-community@lists.sr.ht](mailto:~sirodoht/mataroa-community@lists.sr.ht)
+for the mataroa community to introduce themselves, their blogs, and discuss
+anything that’s on their mind!
+
+Archives at
+[lists.sr.ht/~sirodoht/mataroa-community](https://lists.sr.ht/~sirodoht/mataroa-community)
+
+### Tools
+
+* [mataroa-cli](https://github.com/mataroablog/mataroa-cli)
+* [Mataroa Telegram Bot](https://github.com/Unknowing9428/Mataroa-Telegram-Bot)
+
+## Contributing
+
+Open a PR on [GitHub](https://github.com/mataroablog/mataroa).
+
+Send an email patch to
+[~sirodoht/public-inbox@lists.sr.ht](mailto:~sirodoht/public-inbox@lists.sr.ht).
+See how to contribute using email patches here:
+[git-send-email.io](https://git-send-email.io/).
+
+Read our docs at [docs.mataroa.blog](https://docs.mataroa.blog/)
+
+## Development
+
+This is a [Django](https://www.djangoproject.com/) codebase. Check out the
+[Django docs](https://docs.djangoproject.com/) for general technical
+documentation.
+
+### Structure
+
+The Django project is [`mataroa`](mataroa). There is one Django app,
+[`main`](main), with all business logic. Application CLI commands are generally
+divided into two categories, those under `python manage.py` and those under
+`make`.
+
+### Set up subdomains
+
+Because mataroa works primarily with subdomain, one cannot access the basic web app
+using the standard `http://127.0.0.1:8000` or `http://localhost:8000` URLs. What we do
+for local development is adding a few custom entries on our `/etc/hosts` system file.
+
+Important note: there needs to be an entry of each user account created in the local
+development environment, so that the web server can respond to it.
+
+The first line is the main needed: `mataroalocal.blog`. The rest are included as
+examples of other users one can create in their local environment. The
+easiest way to create them is to go through the sign up page
+(`http://mataroalocal.blog:8000/accounts/create/` using default values).
+
+```
+# /etc/hosts
+
+127.0.0.1 mataroalocal.blog
+
+127.0.0.1 paul.mataroalocal.blog
+127.0.0.1 random.mataroalocal.blog
+127.0.0.1 anyusername.mataroalocal.blog
+```
+
+This will enable us to access mataroa locally (once we start the web server) at
+[http://mataroalocal.blog:8000/](http://mataroalocal.blog:8000/)
+and if we make a user account with username `paul`, then we will be able to access it at
+[http://paul.mataroalocal.blog:8000/](http://paul.mataroalocal.blog:8000/)
+
+### Docker
+
+> [!NOTE]
+> This is the last step for initial Docker setup. See the "Environment variables"
+> section below, for further configuration details.
+
+To set up a development environment with Docker and Docker Compose, run the following
+to start the web server and database:
+
+```
+docker compose up
+```
+
+If you have also configured hosts as described above in the "Set up subdomains"
+section, mataroa should now be locally accessible at
+[http://mataroalocal.blog:8000/](http://mataroalocal.blog:8000/)
+
+Note: The database data are saved in the git-ignored `docker-postgres-data` docker
+volume, located in the root of the project.
+
+### Dependencies
+
+We use `uv` for dependency management and virtual environments.
+
+```
+uv sync --all-groups
+```
+
+### Environment variables
+
+A file named `.envrc` is used to define the environment variables required for
+this project to function. One can either export it directly or use
+[direnv](https://github.com/direnv/direnv). There is an example environment
+file one can copy as base:
+
+```sh
+cp .envrc.example .envrc
+```
+
+`.envrc` should contain the following variables:
+
+```sh
+# .envrc
+
+export DEBUG=1
+export SECRET_KEY=some-secret-key
+export DATABASE_URL=postgres://mataroa:db-password@db:5432/mataroa
+export EMAIL_HOST_USER=smtp-user
+export EMAIL_HOST_PASSWORD=smtp-password
+```
+
+When on production, also include/update the following variables (see
+[Deployment](#Deployment) and [Backup](#Backup)):
+
+```sh
+# .envrc
+
+export DEBUG=0
+export PGPASSWORD=db-password
+```
+
+When on Docker, to change or populate environment variables, edit the `environment`
+key of the `web` service either directly on `docker-compose.yml` or by overriding it
+using the standard named git-ignored `docker-compose.override.yml`.
+
+```sh
+# docker-compose.override.yml
+
+version: "3.8"
+
+services:
+ web:
+ environment:
+ EMAIL_HOST_USER=smtp-user
+ EMAIL_HOST_PASSWORD=smtp-password
+```
+
+Finally, stop and start `docker compose up` again. It should pick up the override file
+as it has the default name `docker-compose.override.yml`.
+
+### Database
+
+This project is using one PostreSQL database for persistence.
+
+One can use the `make pginit` command to initialise a database in the
+`postgres-data/` directory.
+
+After setting the `DATABASE_URL` ([see above](#environment-variables)), create
+the database schema with:
+
+```sh
+uv python manage.py migrate
+```
+
+Initialising the database with some sample development data is possible with:
+
+```sh
+uv python manage.py loaddata dev-data
+```
+
+* `dev-data` is defined in [`main/fixtures/dev-data.json`](main/fixtures/dev-data.json)
+* Credentials of the fixtured user are `admin` / `admin`.
+
+### Serve
+
+To run the Django development server:
+
+```sh
+uv python manage.py runserver
+```
+
+If you have also configured hosts as described above in the "Set up subdomains"
+section, mataroa should now be locally accessible at
+[http://mataroalocal.blog:8000/](http://mataroalocal.blog:8000/)
+
+## Testing
+
+Using the Django test runner:
+
+```sh
+uv run python manage.py test
+```
+
+For coverage, run:
+
+```sh
+uv run coverage run --source='.' --omit '.venv/*' manage.py test
+uv run coverage report -m
+```
+
+## Code linting & formatting
+
+We use [ruff](https://github.com/astral-sh/ruff) for Python code formatting and linting.
+
+To format:
+
+```sh
+uv run ruff format
+```
+
+To lint:
+
+```sh
+uv run ruff check
+uv run ruff check --fix
+```
+
+## Python dependencies
+
+We use `uv` to manage dependencies declared in `pyproject.toml` (see `[project]` and `[dependency-groups]`).
+
+Common commands:
+
+```sh
+# Add or remove dependencies
+uv add
+uv remove
+
+# Update locked versions and install
+uv lock -U
+uv sync --all-groups
+```
+
+## Deployment
+
+See the [Deployment](./docs/deployment.md) document for an overview on steps
+required to deploy a mataroa instance.
+
+### Useful Commands
+
+To reload the gunicorn process:
+
+```sh
+sudo systemctl reload mataroa
+```
+
+To reload Caddy:
+
+```sh
+systemctl restart caddy # root only
+```
+
+gunicorn logs:
+
+```sh
+journalctl -fb -u mataroa
+```
+
+Caddy logs:
+
+```sh
+journalctl -fb -u caddy
+```
+
+Get an overview with systemd status:
+
+```sh
+systemctl status caddy
+systemctl status mataroa
+```
+
+## Backup
+
+See [Database Backup](docs/database-backup.md) for details. In summary:
+
+To create a database dump:
+
+```sh
+pg_dump -Fc --no-acl mataroa -h localhost -U mataroa -f /home/deploy/mataroa.dump -w
+```
+
+To restore a database dump:
+
+```sh
+pg_restore -v -h localhost -cO --if-exists -d mataroa -U mataroa -W mataroa.dump
+```
+
+## Management
+
+In addition to the standard Django management commands, there are also:
+
+* `processnotifications`: sends notification emails for new blog posts of existing records.
+* `mailexports`: emails users of their blog exports.
+
+They are triggered using the standard `manage.py` Django way; eg:
+
+```sh
+python manage.py processnotifications
+```
+
+## Billing
+
+One can deploy mataroa without setting up billing functionalities. This is
+the default case. To handle payments and subscriptions this project uses
+[Stripe](https://stripe.com/). To enable Stripe and payments, one needs to have
+a Stripe account with a single
+[Product](https://stripe.com/docs/billing/prices-guide) (eg. "Mataroa Premium
+Plan").
+
+To configure, add the following variables from your Stripe account to your
+`.envrc`:
+
+```sh
+export STRIPE_API_KEY="sk_test_XXX"
+export STRIPE_PUBLIC_KEY="pk_test_XXX"
+export STRIPE_PRICE_ID="price_XXX"
+```
+
+## License
+
+Copyright Mataroa Contributors
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, version 3.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000000000000000000000000000000000..c7b8ce681a11af490c48320e617c17cb94bc1230
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,6 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+To report a security vulnerability please send an email
+to [admin+security@bocpress.co.uk](mailto:admin+security@bocpress.co.uk).
diff --git a/ansible/.envrc.example b/ansible/.envrc.example
new file mode 100644
index 0000000000000000000000000000000000000000..d6f64436c8b735e4864afc3c454f7d97eb3509c4
--- /dev/null
+++ b/ansible/.envrc.example
@@ -0,0 +1,42 @@
+# inventory.yaml
+
+# Server IP and user with ssh access
+export ANSIBLE_HOST=
+export ANSIBLE_USER=root
+
+
+# vars.yaml
+
+# Domain name and email for Caddy
+export DOMAIN=mataroa.blog
+export EMAIL=admin@mataroa.blog
+
+# Show exceptions and tracebacks on errors
+# 1: show
+# 0: don't show
+export DEBUG=1
+
+# Stop real emails and turn https off
+# 1: stop and off
+# 0: do not stop and on
+export LOCALDEV=1
+
+# Session cookies secret
+export SECRET_KEY=some-secret-key
+
+# Database connection
+export DATABASE_URL=postgres://mataroa:xxx@localhost:5432/mataroa
+export POSTGRES_USERNAME=mataroa
+export POSTGRES_PASSWORD=xxx
+
+# SMTP credentials
+export EMAIL_HOST_USER=
+export EMAIL_HOST_PASSWORD=
+
+# Receivers for email tests (comma separated)
+export EMAIL_TEST_RECEIVE_LIST=
+
+# Stripe payments details
+export STRIPE_API_KEY=
+export STRIPE_PUBLIC_KEY=
+export STRIPE_PRICE_ID=
diff --git a/ansible/Caddyfile.j2 b/ansible/Caddyfile.j2
new file mode 100644
index 0000000000000000000000000000000000000000..a8c37782ab236e6be27ecb1babda6029d4fe63c0
--- /dev/null
+++ b/ansible/Caddyfile.j2
@@ -0,0 +1,43 @@
+{
+ on_demand_tls {
+ ask https://{{ domain }}/accounts/domain/
+ }
+}
+
+*.{{ domain }}, {{ domain }} {
+ route {
+ file_server /static/* {
+ root /var/www/mataroa
+ }
+ reverse_proxy 127.0.0.1:5000
+ }
+
+ tls /etc/caddy/mataroa-blog-cert.pem /etc/caddy/mataroa-blog-key.pem
+
+ encode zstd gzip
+
+ log {
+ output stdout
+ format console
+ }
+}
+
+:443 {
+ route {
+ file_server /static/* {
+ root /var/www/mataroa
+ }
+ reverse_proxy 127.0.0.1:5000
+ }
+
+ tls {{ email }} {
+ on_demand
+ }
+
+ encode zstd gzip
+
+ log {
+ output stdout
+ format console
+ }
+}
diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..f9e6eec0c73148340e98b47d48dff7e3e69ffd3c
--- /dev/null
+++ b/ansible/ansible.cfg
@@ -0,0 +1,3 @@
+[defaults]
+inventory = inventory.yaml
+pipelining = True
diff --git a/ansible/backup-database.sh b/ansible/backup-database.sh
new file mode 100755
index 0000000000000000000000000000000000000000..69cd524367e54cf020a6dddf14d98b871051243c
--- /dev/null
+++ b/ansible/backup-database.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+ set -o xtrace
+fi
+
+if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then
+ echo 'Usage: ./backup-database.sh
+
+This script dumps the mataroa postgres database and uploads it into an S3-compatible server.'
+ exit
+fi
+
+cd "$(dirname "$0")"
+
+main() {
+ # source for PGPASSWORD variable if present
+ if [ -f /var/www/mataroa/.envrc ]; then
+ # shellcheck disable=SC1091
+ source /var/www/mataroa/.envrc
+ fi
+
+ # dump database in the home folder
+ pg_dump -Fc --no-acl mataroa -h localhost -U mataroa -f /home/deploy/mataroa.dump -w
+
+ # upload using aws cli
+ /usr/bin/rclone copy --progress /home/deploy/mataroa.dump scaleway:bucket/mataroa-backups/postgres-mataroa-"$(date --utc +%Y%m%d-%H%M%S)"/
+}
+
+main "$@"
diff --git a/ansible/caddy.service.j2 b/ansible/caddy.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b67030ea0b08502d273982a75028cd7dbe25bd88
--- /dev/null
+++ b/ansible/caddy.service.j2
@@ -0,0 +1,22 @@
+[Unit]
+Description=Caddy
+Documentation=https://caddyserver.com/docs/
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+Type=notify
+User=caddy
+Group=caddy
+ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
+ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
+Restart=on-failure
+RestartSec=5
+TimeoutStopSec=5s
+LimitNOFILE=1048576
+PrivateTmp=true
+ProtectSystem=full
+AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/inventory.yaml b/ansible/inventory.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..17a3ccc307e34372629e569fba2d026396ecfdce
--- /dev/null
+++ b/ansible/inventory.yaml
@@ -0,0 +1,5 @@
+virtualmachines:
+ hosts:
+ main:
+ ansible_host: "{{ lookup('env', 'ANSIBLE_HOST') }}"
+ ansible_user: "{{ lookup('env', 'ANSIBLE_USER') }}"
diff --git a/ansible/mataroa-backup.service.j2 b/ansible/mataroa-backup.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..50fa84cac24432a39107d2feecbf6b6dc5fe29bd
--- /dev/null
+++ b/ansible/mataroa-backup.service.j2
@@ -0,0 +1,12 @@
+[Unit]
+Description=Backup mataroa database
+
+[Service]
+Type=oneshot
+User=deploy
+WorkingDirectory=/home/deploy
+Environment="PGPASSWORD={{ postgres_password }}"
+ExecStart=/home/deploy/backup-database.sh
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/mataroa-backup.timer.j2 b/ansible/mataroa-backup.timer.j2
new file mode 100644
index 0000000000000000000000000000000000000000..dec812be0164083070345e7298a0a632834f686f
--- /dev/null
+++ b/ansible/mataroa-backup.timer.j2
@@ -0,0 +1,8 @@
+[Unit]
+Description=Run mataroa-backup every 6 hours
+
+[Timer]
+OnCalendar=*-*-* 00/6:00:00
+
+[Install]
+WantedBy=timers.target
diff --git a/ansible/mataroa-dailysummary.service.j2 b/ansible/mataroa-dailysummary.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..5f7d4135b44b28e751c254a743662de72eeb8627
--- /dev/null
+++ b/ansible/mataroa-dailysummary.service.j2
@@ -0,0 +1,12 @@
+[Unit]
+Description=Send mataroa daily moderation summary
+
+[Service]
+Type=oneshot
+User=deploy
+WorkingDirectory=/var/www/mataroa
+EnvironmentFile=/etc/systemd/system/mataroa.env
+ExecStart=/home/deploy/.local/bin/uv run manage.py mailsummary
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/mataroa-dailysummary.timer.j2 b/ansible/mataroa-dailysummary.timer.j2
new file mode 100644
index 0000000000000000000000000000000000000000..2921a8e94f52261c661d7b3bab0f05ee0d8401a6
--- /dev/null
+++ b/ansible/mataroa-dailysummary.timer.j2
@@ -0,0 +1,8 @@
+[Unit]
+Description=Run mataroa-dailysummary every day at 00:15 UTC
+
+[Timer]
+OnCalendar=*-*-* 00:15:00
+
+[Install]
+WantedBy=timers.target
diff --git a/ansible/mataroa-exports.service.j2 b/ansible/mataroa-exports.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..d1ad4e56f1c9e7997a684220625ac7688a53711b
--- /dev/null
+++ b/ansible/mataroa-exports.service.j2
@@ -0,0 +1,12 @@
+[Unit]
+Description=Send mataroa exports
+
+[Service]
+Type=oneshot
+User=deploy
+WorkingDirectory=/var/www/mataroa
+EnvironmentFile=/etc/systemd/system/mataroa.env
+ExecStart=/home/deploy/.local/bin/uv run manage.py mailexports
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/mataroa-exports.timer.j2 b/ansible/mataroa-exports.timer.j2
new file mode 100644
index 0000000000000000000000000000000000000000..a51d153ebef22df9160f64600bd64731fedbb884
--- /dev/null
+++ b/ansible/mataroa-exports.timer.j2
@@ -0,0 +1,8 @@
+[Unit]
+Description=Run mataroa-exports every first of the month
+
+[Timer]
+OnCalendar=*-*-01 06:00:00
+
+[Install]
+WantedBy=timers.target
diff --git a/ansible/mataroa-notifications.service.j2 b/ansible/mataroa-notifications.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..d1648b5a085d77e0ddee2c619518d2eda760eb29
--- /dev/null
+++ b/ansible/mataroa-notifications.service.j2
@@ -0,0 +1,12 @@
+[Unit]
+Description=Process mataroa notifications
+
+[Service]
+Type=oneshot
+User=deploy
+WorkingDirectory=/var/www/mataroa
+EnvironmentFile=/etc/systemd/system/mataroa.env
+ExecStart=/home/deploy/.local/bin/uv run manage.py processnotifications --no-dryrun
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/mataroa-notifications.timer.j2 b/ansible/mataroa-notifications.timer.j2
new file mode 100644
index 0000000000000000000000000000000000000000..ab166a24195cc0014c52175bd6948165f58f5cce
--- /dev/null
+++ b/ansible/mataroa-notifications.timer.j2
@@ -0,0 +1,8 @@
+[Unit]
+Description=Run mataroa-notifications every day
+
+[Timer]
+OnCalendar=*-*-* 10:00:00
+
+[Install]
+WantedBy=timers.target
diff --git a/ansible/mataroa.env.j2 b/ansible/mataroa.env.j2
new file mode 100644
index 0000000000000000000000000000000000000000..5d002c25d087e8d8c0c0126215918f374ef13747
--- /dev/null
+++ b/ansible/mataroa.env.j2
@@ -0,0 +1,12 @@
+DOMAIN={{ domain }}
+EMAIL={{ email }}
+DEBUG={{ debug }}
+LOCALDEV={{ localdev }}
+SECRET_KEY={{ secret_key }}
+DATABASE_URL={{ database_url }}
+EMAIL_HOST_USER={{ email_host_user }}
+EMAIL_HOST_PASSWORD={{ email_host_password }}
+EMAIL_TEST_RECEIVE_LIST={{ email_test_receive_list }}
+STRIPE_API_KEY={{ stripe_api_key }}
+STRIPE_PUBLIC_KEY={{ stripe_public_key }}
+STRIPE_PRICE_ID={{ stripe_price_id }}
diff --git a/ansible/mataroa.service.j2 b/ansible/mataroa.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..3b2f56d80f158ac26c823ab414c155eaec1bd95b
--- /dev/null
+++ b/ansible/mataroa.service.j2
@@ -0,0 +1,17 @@
+[Unit]
+Description=mataroa
+After=network.target
+
+[Service]
+Type=simple
+User=deploy
+Group=www-data
+WorkingDirectory=/var/www/mataroa
+ExecStart=/home/deploy/.local/bin/uv run gunicorn -b 127.0.0.1:5000 -w 4 --access-logfile - mataroa.wsgi
+ExecReload=/bin/kill -HUP $MAINPID
+EnvironmentFile=/etc/systemd/system/mataroa.env
+TimeoutSec=15
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..84cca796595945982f915ba14d293d9a53eefd41
--- /dev/null
+++ b/ansible/playbook.yaml
@@ -0,0 +1,201 @@
+---
+- hosts: virtualmachines
+ vars_files:
+ - vars.yaml
+ vars:
+ app_dir: /var/www/mataroa
+ django_manage: /home/deploy/.local/bin/uv run python manage.py
+ systemd_unit_templates:
+ - mataroa-notifications.timer.j2
+ - mataroa-notifications.service.j2
+ - mataroa-exports.timer.j2
+ - mataroa-exports.service.j2
+ - mataroa-backup.timer.j2
+ - mataroa-backup.service.j2
+ - mataroa-dailysummary.timer.j2
+ - mataroa-dailysummary.service.j2
+ become: yes
+ tasks:
+ # smoke test and essential dependencies
+ - name: ping
+ ansible.builtin.ping:
+ - name: essentials
+ ansible.builtin.apt:
+ update_cache: yes
+ name:
+ - gcc
+ - git
+ - rclone
+ - vim
+ state: present
+
+ # caddy
+ - name: add caddy key
+ ansible.builtin.apt_key:
+ id: 65760C51EDEA2017CEA2CA15155B6D79CA56EA34
+ url: https://dl.cloudsmith.io/public/caddy/stable/gpg.key
+ keyring: /etc/apt/trusted.gpg.d/caddy-stable.gpg
+ state: present
+ - name: add caddy repositories
+ ansible.builtin.apt_repository:
+ repo: "{{ item }}"
+ loop:
+ - "deb [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main"
+ - "deb-src [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main"
+ - name: install caddy
+ ansible.builtin.apt:
+ update_cache: yes
+ name: caddy
+ - name: caddyfile
+ ansible.builtin.template:
+ src: Caddyfile.j2
+ dest: /etc/caddy/Caddyfile
+ owner: root
+ group: root
+ mode: '0644'
+ notify: restart caddy
+ - name: systemd environment file for mataroa
+ ansible.builtin.template:
+ src: mataroa.env.j2
+ dest: /etc/systemd/system/mataroa.env
+ owner: root
+ group: root
+ mode: '0640'
+
+ # deploy user and directory
+ - name: create user
+ ansible.builtin.user:
+ name: deploy
+ password: ""
+ shell: /bin/bash
+ groups:
+ - sudo
+ - www-data
+ append: yes
+ createhome: yes
+ skeleton: '/etc/skel'
+ generate_ssh_key: yes
+ ssh_key_type: 'ed25519'
+ - name: www directory
+ ansible.builtin.file:
+ path: /var/www
+ state: directory
+ mode: '0755'
+ owner: deploy
+ group: www-data
+
+ # postgresql setup
+ - name: pg user
+ community.general.postgresql_user:
+ name: "{{ postgres_username }}"
+ password: "{{ postgres_password }}"
+ expires: infinity
+ state: present
+ become_user: postgres
+ - name: pg database
+ community.general.postgresql_db:
+ name: mataroa
+ owner: "{{ postgres_username }}"
+ state: present
+ become_user: postgres
+ - name: pg permissions
+ community.postgresql.postgresql_privs:
+ db: mataroa
+ privs: ALL
+ objs: ALL_IN_SCHEMA
+ role: "{{ postgres_username }}"
+ grant_option: true
+ become_user: postgres
+
+ # repo and tooling (as deploy user)
+ - name: setup repo and tooling
+ become_user: deploy
+ block:
+ - name: uv
+ ansible.builtin.shell:
+ cmd: curl -LsSf https://astral.sh/uv/0.8.8/install.sh | sh
+ - name: clone
+ ansible.builtin.git:
+ repo: https://github.com/mataroablog/mataroa
+ dest: /var/www/mataroa
+ version: main
+ accept_hostkey: true
+
+ # systemd
+ - name: systemd main service
+ ansible.builtin.template:
+ src: mataroa.service.j2
+ dest: /etc/systemd/system/mataroa.service
+ owner: root
+ group: root
+ mode: '0644'
+ notify:
+ - reload systemd
+ - restart mataroa
+ - name: systemd timers and helper services
+ ansible.builtin.template:
+ src: "{{ item }}"
+ dest: "/etc/systemd/system/{{ item | regex_replace('\\.j2$', '') }}"
+ owner: root
+ group: root
+ mode: '0644'
+ loop: "{{ systemd_unit_templates }}"
+ notify: reload systemd
+ - name: install backup script
+ ansible.builtin.copy:
+ src: backup-database.sh
+ dest: /home/deploy/backup-database.sh
+ owner: deploy
+ group: deploy
+ mode: '0755'
+ - name: flush handlers before enabling timers
+ ansible.builtin.meta: flush_handlers
+ - name: systemd enable and start timers
+ ansible.builtin.systemd:
+ name: "{{ item }}"
+ enabled: yes
+ state: started
+ loop:
+ - mataroa-notifications.timer
+ - mataroa-exports.timer
+ - mataroa-backup.timer
+ - mataroa-dailysummary.timer
+ - name: systemd enable
+ ansible.builtin.systemd:
+ name: mataroa
+ enabled: yes
+
+ # deployment specific
+ - name: collectstatic
+ ansible.builtin.shell:
+ cmd: "{{ django_manage }} collectstatic --no-input"
+ chdir: "{{ app_dir }}"
+ args:
+ executable: /bin/bash
+ become_user: deploy
+ - name: migrations
+ ansible.builtin.shell:
+ cmd: "{{ django_manage }} migrate --no-input"
+ chdir: "{{ app_dir }}"
+ args:
+ executable: /bin/bash
+ environment:
+ DATABASE_URL: "{{ database_url }}"
+ become_user: deploy
+ - name: caddy enable
+ ansible.builtin.systemd:
+ name: caddy
+ enabled: yes
+
+ handlers:
+ - name: reload systemd
+ ansible.builtin.systemd:
+ daemon_reload: true
+ - name: restart mataroa
+ ansible.builtin.systemd:
+ name: mataroa
+ state: restarted
+ - name: restart caddy
+ ansible.builtin.systemd:
+ name: caddy
+ state: restarted
diff --git a/ansible/vars.yaml b/ansible/vars.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0717c5ba735bdc0a580d1b65b0802fab5b3535f3
--- /dev/null
+++ b/ansible/vars.yaml
@@ -0,0 +1,20 @@
+---
+domain: "{{ lookup('env', 'DOMAIN') }}"
+email: "{{ lookup('env', 'EMAIL') }}"
+
+debug: "{{ lookup('env', 'DEBUG') }}"
+localdev: "{{ lookup('env', 'LOCALDEV') }}"
+
+secret_key: "{{ lookup('env', 'SECRET_KEY') }}"
+
+database_url: "{{ lookup('env', 'DATABASE_URL') }}"
+postgres_username: "{{ lookup('env', 'POSTGRES_USERNAME') }}"
+postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') }}"
+
+email_host_user: "{{ lookup('env', 'EMAIL_HOST_USER') }}"
+email_host_password: "{{ lookup('env', 'EMAIL_HOST_PASSWORD') }}"
+email_test_receive_list: "{{ lookup('env', 'EMAIL_TEST_RECEIVE_LIST') }}"
+
+stripe_api_key: "{{ lookup('env', 'STRIPE_API_KEY') }}"
+stripe_public_key: "{{ lookup('env', 'STRIPE_PUBLIC_KEY') }}"
+stripe_price_id: "{{ lookup('env', 'STRIPE_PRICE_ID') }}"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..971e76adb0dd84d70df28790428b39f43e970681
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+services:
+ db:
+ image: postgres:17
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ environment:
+ POSTGRES_PASSWORD: postgres
+ volumes:
+ - ./docker-postgres-data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ web:
+ command: >
+ bash -c "uv run --active python manage.py migrate && \
+ uv run --active python manage.py collectstatic --noinput && \
+ uv run --active python manage.py runserver 0.0.0.0:8000"
+ build: .
+ image: mataroa
+ volumes:
+ - .:/code
+ ports:
+ - "8000:8000"
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ DEBUG: 1
+ LOCALDEV: 1
+ DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
diff --git a/docs/book.toml b/docs/book.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c5707bcb7eca8f41473b9a06c09bb68f4ac7714e
--- /dev/null
+++ b/docs/book.toml
@@ -0,0 +1,6 @@
+[book]
+authors = ["Theodore Keloglou"]
+language = "en"
+multilingual = false
+src = "src"
+title = "Mataroa Documentation"
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
new file mode 100644
index 0000000000000000000000000000000000000000..f93d1b632988f63d79a3c97358517bb6d033c456
--- /dev/null
+++ b/docs/src/SUMMARY.md
@@ -0,0 +1,13 @@
+# Summary
+
+- [Introduction](./introduction.md)
+- [Main Repository README](./main-repository-readme.md)
+- [Coding Conventions](./coding-conventions.md)
+- [Git Commit Message Guidelines](./commit-messages.md)
+- [File Structure Walkthrough](./file-structure-walkthrough.md)
+- [Dependencies](./dependencies.md)
+- [Deployment](./deployment.md)
+- [Cronjobs](./cronjobs.md)
+- [Database Backup](./database-backup.md)
+- [Server Migration](./server-migration.md)
+- [Runbook](./runbook.md)
diff --git a/docs/src/coding-conventions.md b/docs/src/coding-conventions.md
new file mode 100644
index 0000000000000000000000000000000000000000..0a8d5617650de50166564038272c7cb903b7f251
--- /dev/null
+++ b/docs/src/coding-conventions.md
@@ -0,0 +1,4 @@
+# Coding Conventions
+
+1. All files should end with a new line character.
+1. Python code should be formatted with [ruff](https://github.com/astral-sh/ruff).
diff --git a/docs/src/commit-messages.md b/docs/src/commit-messages.md
new file mode 100644
index 0000000000000000000000000000000000000000..6b9267dde634b70231c52958240cb688d4f850f4
--- /dev/null
+++ b/docs/src/commit-messages.md
@@ -0,0 +1,15 @@
+# Git Commit Message Guidelines
+
+We follow some simple non-austere git commit message guidelines.
+
+* Start with a verb
+ * `add`
+ * `change`
+ * `delete`
+ * `fix`
+ * `refactor`
+ * `tweak`
+ * et al.
+* Start with a lowercase letter
+ * eg. `change analytic page path to the same of page slug`
+* Do not end with a fullstop
diff --git a/docs/src/cronjobs.md b/docs/src/cronjobs.md
new file mode 100644
index 0000000000000000000000000000000000000000..9a5f641deea6c1430a38852de4dddbe1c29cbce1
--- /dev/null
+++ b/docs/src/cronjobs.md
@@ -0,0 +1,31 @@
+# Cronjobs
+
+We don't use cron but systemd timers for jobs that need to run recurringly.
+
+## Process email notifications
+
+```sh
+python manage.py processnotifications
+```
+
+Sends notification emails for new blog posts.
+
+Triggers daily at 10AM server time.
+
+## Email blog exports
+
+```sh
+python manage.py mailexports
+```
+
+Emails users their blog exports.
+
+Triggers monthly, first day of the month, 6AM server time.
+
+## Database backup
+
+```
+./backup-database.sh
+```
+
+Triggers every 6 hours.
diff --git a/docs/src/database-backup.md b/docs/src/database-backup.md
new file mode 100644
index 0000000000000000000000000000000000000000..ade2e57f209b4be9f6b77fbd6b372367f9183ba7
--- /dev/null
+++ b/docs/src/database-backup.md
@@ -0,0 +1,45 @@
+# Database Backup
+
+## Shell Script
+
+We use the script [`backup-database.sh`](backup-database.sh) to dump the
+database and upload it into an S3-compatible object storage cloud using
+[rclone](https://rclone.org/). This script needs the database password
+as an environment variable. The key must be `PGPASSWORD`. The variable can live
+in `.envrc` as such:
+
+```sh
+export PGPASSWORD=db-password
+```
+
+## Commands
+
+To create a database dump run:
+
+```sh
+pg_dump -Fc --no-acl mataroa -h localhost -U mataroa -f /home/deploy/mataroa.dump -W
+```
+
+To restore a database dump run:
+
+```sh
+pg_restore --disable-triggers -j 4 -v -h localhost -cO --if-exists -d mataroa -U mataroa -W mataroa.dump
+```
+
+## Initialise and configure backup script
+
+```sh
+cp /var/www/mataroa/backup-database.sh /home/deploy/
+```
+
+## Setup rclone
+
+1. Create bucket on Scaleway or any other S3-compatible object storage.
+1. Find bucket URL.
+ * On Scaleway: it's on Bucket Settings.
+1. Acquire IAM Access Key ID and Secret Key.
+ * On Scaleway: IAM -> Applications -> Project default -> API Keys
+
+```sh
+rclone config
+```
diff --git a/docs/src/dependencies.md b/docs/src/dependencies.md
new file mode 100644
index 0000000000000000000000000000000000000000..b12420c9adeb21b44564b896964725daeacb39a9
--- /dev/null
+++ b/docs/src/dependencies.md
@@ -0,0 +1,47 @@
+# Dependencies
+
+## Dependency Policy
+
+The mataroa project has an unusually strict yet usually unclear dependency policy.
+
+Vague rules include:
+
+* No third-party Django apps.
+* All Python / PyPI packages should be individually vetted.
+ * Packages should be published from community-trusted organisations or developers.
+ * Packages should be actively maintained (though not necessarily actively developed).
+ * Packages should hold a high quality of coding practices.
+* No JavaScript libraries / dependencies.
+
+Current list of top-level PyPI dependencies (source at [`pyproject.toml`](/pyproject.toml)):
+
+* [Django](https://pypi.org/project/Django/)
+* [psycopg](https://pypi.org/project/psycopg/)
+* [gunicorn](https://pypi.org/project/gunicorn/)
+* [Markdown](https://pypi.org/project/Markdown/)
+* [Pygments](https://pypi.org/project/Pygments/)
+* [bleach](https://pypi.org/project/bleach/)
+* [stripe](https://pypi.org/project/stripe/)
+
+## Adding a new dependency
+
+After approving a dependency, add it using `uv`:
+
+1. Ensure `uv` is installed and a virtualenv exists (managed by `uv`).
+1. Add the dependency to `pyproject.toml` and lockfile with:
+ - Runtime: `uv add PACKAGE`
+ - Dev-only: `uv add --dev PACKAGE`
+1. Install/sync dependencies: `uv sync`
+
+## Upgrading dependencies
+
+When a new Django version is out it’s a good idea to upgrade everything.
+
+Steps:
+
+1. Update the lockfile: `uv lock --upgrade`
+1. Review changes: `git diff uv.lock` and spot non-patch level version bumps.
+1. Examine release notes of each one.
+1. Install updated deps: `uv sync`
+1. Unless something comes up, make sure tests and smoke tests pass.
+1. Deploy new dependency versions.
diff --git a/docs/src/deployment.md b/docs/src/deployment.md
new file mode 100644
index 0000000000000000000000000000000000000000..228363c1bed7b78f28a9428a40677faad64cd0a0
--- /dev/null
+++ b/docs/src/deployment.md
@@ -0,0 +1,66 @@
+# Deployment
+
+## Step 1: Ansible
+
+We use ansible to provision a Debian 12 Linux server.
+
+(1a) First, set up configuration files:
+
+```sh
+cd ansible/
+# Make a copy of the example file
+cp .envrc.example .envrc
+
+# Edit parameters as required
+vim .envrc
+
+# Load variables into environment
+source .envrc
+```
+
+(1b) Then, provision:
+
+```sh
+ansible-playbook playbook.yaml -v
+```
+
+## Step 2: Wildcard certificates
+
+We use Automatic DNS API integration with DNSimple:
+
+* https://github.com/acmesh-official/acme.sh?tab=readme-ov-file#1-how-to-install
+* https://github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
+
+Note: acme.sh's default SSL provider is ZeroSSL which does not accept email with
+plus-subaddressing. It will not error gracefully, just fail with a cryptic
+message (tested with acmesh v3.0.7).
+
+```sh
+curl https://get.acme.sh | sh -s email=person@example.com
+# Note: Installation inserts a cronjob for auto-renewal
+
+# Setup DNSimple API
+echo 'export DNSimple_OAUTH_TOKEN="token-here"' >> /root/.acme.sh/acme.sh.env
+
+# Issue cert
+acme.sh --issue --dns dns_dnsimple -d mataroa.blog -d *.mataroa.blog
+
+# We "install" (copy) the cert because we should not use the cert from acme.sh's internal store
+acme.sh --install-cert -d mataroa.blog -d *.mataroa.blog --key-file /etc/caddy/mataroa-blog-key.pem --fullchain-file /etc/caddy/mataroa-blog-cert.pem --reloadcmd "chown caddy:www-data /etc/caddy/mataroa-blog-{cert,key}.pem && systemctl restart caddy"
+```
+
+## Step 3: Cronjobs and Automated backups
+
+There are a few cronjobs that need setting up and, of course, backups are essential:
+
+* (3a) [Cronjobs](./cronjobs.md)
+* (3b) [Database Backup](./database-backup.md)
+
+## Step 4: Deploy changes
+
+```sh
+git push origin main
+source .venv/bin/activate
+cd ansible/
+ansible-playbook -v deploy.yaml
+```
diff --git a/docs/src/file-structure-walkthrough.md b/docs/src/file-structure-walkthrough.md
new file mode 100644
index 0000000000000000000000000000000000000000..fd5717bbf581ab6f9c4bc1dead2619e5d8d0c51b
--- /dev/null
+++ b/docs/src/file-structure-walkthrough.md
@@ -0,0 +1,191 @@
+# File Structure Walkthrough
+
+Here, an overview of the project's code sources is presented. The purpose is
+for the reader to understand what kind of functionality is located where in
+the sources.
+
+All business logic of the application is in one Django app: [`main`](/main).
+
+Condensed and commented sources file tree:
+
+```
+.
+├── .build.yml # SourceHut CI build config
+├── .envrc.example # example direnv file
+├── .github/ # GitHub Actions config files
+├── Caddyfile # configuration for Caddy webserver
+├── Dockerfile
+├── LICENSE
+├── Makefile # make-defined tasks
+├── README.md
+├── backup-database.sh
+├── default.nix # nix profile
+├── deploy.sh
+├── docker-compose.yml
+├── docs/
+├── export_base_epub/ # base sources for epub export functionality
+├── export_base_hugo/ # base sources for hugo export functionality
+├── export_base_zola/ # base sources for zola export functionality
+├── main/
+│ ├── admin.py
+│ ├── apps.py
+│ ├── denylist.py # list of various keywords allowed and denied
+│ ├── feeds.py # django rss functionality
+│ ├── fixtures/
+│ │ └── dev-data.json # sample development data
+│ ├── forms.py
+│ ├── management/ # commands under `python manage.py`
+│ │ └── commands/
+│ │ └── processnotifications.py
+│ │ └── mailexports.py
+│ ├── middleware.py # mostly subdomain routing
+│ ├── migrations/
+│ ├── models.py
+│ ├── static/
+│ ├── templates
+│ │ ├── main/ # HTML templates for most pages
+│ │ ├── assets/
+│ │ │ ├── drag-and-drop-upload.js
+│ │ │ └── style.css
+│ │ ├── partials/
+│ │ │ ├── footer.html
+│ │ │ ├── footer_blog.html
+│ │ │ └── webring.html
+│ │ └── registration/
+│ ├── tests/
+│ │ ├── test_billing.py
+│ │ ├── test_blog.py
+│ │ ├── test_comments.py
+│ │ ├── test_images.py
+│ │ ├── test_management.py
+│ │ ├── test_pages.py
+│ │ ├── test_posts.py
+│ │ ├── test_users.py
+│ │ └── testdata/
+│ ├── urls.py
+│ ├── util.py
+│ ├── validators.py # custom form and field validators
+│ ├── views.py
+│ ├── views_api.py
+│ ├── views_billing.py
+│ └── views_export.py
+├── manage.py
+└── mataroa
+ ├── asgi.py
+ ├── settings.py # django configuration file
+ ├── urls.py
+ └── wsgi.py
+```
+
+## [`main/urls.py`](/main/urls.py)
+
+All urls are in this module. They are visually divided into several sections:
+
+* general, includes index, dashboard, static pages
+* user system, includes signup, settings, logout
+* blog posts, the CRUD opertions of
+* blog extras, includes rss and newsletter features
+* comments, related to the blog post comments
+* billing, subscription and card related
+* blog import, export, webring
+* images CRUD
+* analytics list and details
+* pages CRUD
+
+## [`main/views.py`](/main/views.py)
+
+The majority of business logic is in the `views.py` module.
+
+It includes:
+
+* indexes, dashboard, static pages
+* user CRUD and login/logout
+* posts CRUD
+* comments CRUD
+* images CRUD
+* pages CRUD
+* webring
+* analytics
+* notifications subscribe/unsubscribe
+* moderation dashboard
+* sitemaps
+
+Generally,
+[Django class-based generic views](https://docs.djangoproject.com/en/3.2/topics/class-based-views/generic-display/)
+are used most of the time as they provide useful functionality abstracted away.
+
+The Django source code [for generic views](https://github.com/django/django/tree/main/django/views/generic)
+is also extremely readable:
+
+* [base.py](https://github.com/django/django/blob/main/django/views/generic/base.py): base `View` and `TemplateView`
+* [list.py](https://github.com/django/django/blob/main/django/views/generic/list.py): `ListView`
+* [edit.py](https://github.com/django/django/blob/main/django/views/generic/edit.py): `UpdateView`, `DeleteView`, `FormView`
+* [detail.py](https://github.com/django/django/blob/main/django/views/generic/detail.py): `DetailView`
+
+[Function-based views](https://docs.djangoproject.com/en/3.2/intro/tutorial01/#write-your-first-view)
+are used in cases where the CRUD/RESTful design pattern is not clear such as
+`notification_unsubscribe_key` where we unsubscribe an email via a GET operation.
+
+## [`main/views_api.py`](/main/views_api.py)
+
+This module contains all API related views. These views have their own
+api key based authentication.
+
+## [`main/views_export.py`](/main/views_export.py)
+
+This module contains all views related to the export capabilities of mataroa.
+
+The way the exports work is by reading the base files from the repository root:
+[export_base_hugo](export_base_hugo/), [export_base_zola](export_base_zola/),
+[export_base_epub](export_base_epub/) for Hugo, Zola, and epub respectively.
+After reading, we replace some strings on the configurations, generate posts
+as markdown strings, and zip-archive everything in-memory. Finally, we respond
+using the appropriate content type (`application/zip` or `application/epub`) and
+[Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
+`attachment`.
+
+## [`main/views_billing.py`](/main/views_billing.py)
+
+This module contains all billing and subscription related views. It’s designed to
+support one payment processor, Stripe.
+
+## [`main/tests/`](/main/tests/)
+
+All tests are under this directory. They are divided into several modules,
+based on the functionality and the views they test.
+
+Everything uses the built-in Python `unittest` module along with standard
+Django testing facilities.
+
+## [`main/models.py`](/main/models.py) and [`main/migrations/`](/main/migrations/)
+
+`main/models.py` is where the database schema is defined, translated into
+Django ORM-speak. This always displays the latest schema.
+
+`main/migrations/` includes all incremental migrations required to reach
+the schema defined in `main/models.py` starting from an empty database.
+
+We use the built-in Django commands to generate and execute migrations, namely
+`makemigrations` and `migrate`. For example, the steps to make a schema change
+would be something like:
+
+1. Make the change in `main/models.py`. See
+[Django Model field reference](https://docs.djangoproject.com/en/3.2/ref/models/fields/).
+1. Run `python manage.py makemigrations` to auto-generate the migrations.
+1. Potentially refactor the auto-generated migration file (located at `main/migrations/XXXX_auto_XXXXXXXX.py`)
+1. Run `python manage.py migrate` to execute migrations.
+1. Also `make format` before committing.
+
+## [`main/forms.py`](/main/forms.py)
+
+Here a collection of Django-based forms resides, mostly in regards to user creation,
+upload functionalities (for post import or image upload), and card details
+submission.
+
+See [Django Form fields reference](https://docs.djangoproject.com/en/3.2/ref/forms/fields/).
+
+## [`main/templates/assets/style.css`](main/templates/assets/style.css)
+
+On Mataroa, a user can enable an option, Theme Zia Lucia, and get a higher font
+size by default. Because we need to change the body font-size value, we render
+the CSS. It is not static. This is why it lives inside the templates directory.
diff --git a/docs/src/introduction.md b/docs/src/introduction.md
new file mode 100644
index 0000000000000000000000000000000000000000..e05dd07df4b5c463db30ca1853b50d612127ec2e
--- /dev/null
+++ b/docs/src/introduction.md
@@ -0,0 +1,29 @@
+# Introduction
+
+Welcome to the documentation site of the
+[mataroa blog](https://github.com/mataroablog)
+project!
+
+**Main repository on GitHub**
+[github.com/mataroablog/mataroa](https://github.com/mataroablog/mataroa)
+
+**Mirror repository on sr.ht**
+[git.sr.ht/~sirodoht/mataroa](https://git.sr.ht/~sirodoht/mataroa)
+
+**Report bugs on GitHub**
+[github.com/mataroablog/mataroa/issues](https://github.com/mataroablog/mataroa/issues)
+
+**Contribute on GitHub with Pull Requests**
+[github.com/mataroablog/mataroa/pulls](https://github.com/mataroablog/mataroa/pulls)
+
+**Contribute (platform independent) with email patches**
+[~sirodoht/public-inbox@lists.sr.ht](mailto:~sirodoht/public-inbox@lists.sr.ht)
+
+**Community mailing list**
+[lists.sr.ht/~sirodoht/mataroa-community](https://lists.sr.ht/~sirodoht/mataroa-community)
+
+## Start
+
+Start learning about mataroa with reading the
+main repository
+README:
diff --git a/docs/src/main-repository-readme.md b/docs/src/main-repository-readme.md
new file mode 120000
index 0000000000000000000000000000000000000000..fe840054137e2ccda075344f21e728249a60a2fc
--- /dev/null
+++ b/docs/src/main-repository-readme.md
@@ -0,0 +1 @@
+../../README.md
\ No newline at end of file
diff --git a/docs/src/runbook.md b/docs/src/runbook.md
new file mode 100644
index 0000000000000000000000000000000000000000..125fce752b6d304573ef9f93eea91749aef5f924
--- /dev/null
+++ b/docs/src/runbook.md
@@ -0,0 +1,114 @@
+# Runbook
+
+So, mataroa is down. What do we do?
+
+Firstly, panic. Run around in circles with your hands up in despair. It's important to
+do this, don't think this is a joke! Ok, once that's done:
+
+## 1. Check Caddy
+
+Caddy is the first point of contact inside the server from the outside world.
+
+First ssh into server:
+
+```sh
+ssh root@mataroa.blog
+```
+
+Caddy runs as a systemd service. Check status with:
+
+```sh
+systemctl status caddy
+```
+
+Exit with `q`. If the service is not running and is errored restart with:
+
+```sh
+systemctl restart caddy
+```
+
+If restart does not work, check logs:
+
+```sh
+journalctl -u caddy -r
+```
+
+`-r` is for reverse. Use `-f` to follow logs real time:
+
+```sh
+journalctl -u caddy -f
+```
+
+To search within all logs do slash and then the keyword itself, eg: `/keyword-here`,
+then hit enter.
+
+The config for Caddy is:
+
+```sh
+cat /etc/caddy/Caddyfile
+```
+
+One entry is to serve anything with *.mataroa.blog host, and the second is for anything
+not in that domain, which is exclusively all the blogs custom domains.
+
+The systemd config for Caddy is:
+
+```sh
+cat /etc/systemd/system/multi-user.target.wants/caddy.service
+```
+
+## 2. Check gunicorn
+
+After caddy receives the request, it forwards it to gunicorn. Gunicorn is what runs the
+mataroa Django instances, so it's named `mataroa`. It also runs as a systemd service.
+
+To see status:
+
+```sh
+systemctl status mataroa
+```
+
+To restart:
+
+```sh
+systemctl restart mataroa
+```
+
+To see logs:
+
+```sh
+journalctl -u mataroa -r
+```
+
+and to follow them:
+
+```sh
+journalctl -u mataroa -f
+```
+
+The systemd config for mataroa/gunicorn is:
+
+```sh
+cat /etc/systemd/system/multi-user.target.wants/mataroa.service
+```
+
+Note that the env variables for production live inside the systemd service file.
+
+## 3. How to hotfix code
+
+Here's where the code lives and how to access it:
+
+```sh
+sudo -i -u deploy
+cd /var/www/mataroa/
+source .envrc # load env variables for manual runs
+source .venv/bin/activate # activate venv
+python manage.py
+```
+
+If you make a change in the source code files (inside `/var/www/mataroa`) you need to
+restart the service for the changes to take effect:
+
+```sh
+systemctl restart mataroa
+```
diff --git a/docs/src/server-migration.md b/docs/src/server-migration.md
new file mode 100644
index 0000000000000000000000000000000000000000..c2501455561a9c5db14fb897f8f596f97ad27e36
--- /dev/null
+++ b/docs/src/server-migration.md
@@ -0,0 +1,36 @@
+# Server Migration
+
+Sadly or not, nothing lasts forever. One day you might do a server migration.
+Among many, mataroa is doing something naughty. We store everything, images
+including, in the Postgres database. Naughty indeed, yet makes it much easier to
+backup but also migrate.
+
+To start with, one a migrator has setup their new server (see
+[Deployment](./deployment.md)) we recommend testing everything in another
+domain, other than the main (existing) one.
+
+Once everything works:
+
+1. Verify all production variables and canonical server names exist in settings et al.
+1. Disconnect production server from public IP. This is not a zero-downtime migration — to be clear.
+1. Run backup-database.sh one last time.
+1. Assign elastic/floating IP to new server.
+1. Run TLS certificate (naked and wildcard) generations.
+1. `scp` database dump into new server.
+1. Restore database dump in new server.
+1. Start mataroa and caddy systemd services
+
+Later:
+
+1. Setup cronjobs / systemd timers
+1. Setup healthcheks for recurring jobs.
+1. Verify DEBUG is 0.
+
+The above assume the migrator has a floating IP that they can move around. If
+not, there are two problems. The migrator needs to coordinate DNS but much
+more problematically all custom domains stop working :/ For this reason we
+should implement CNAME custom domains. However, CNAME custom domains do not
+support root domains, so what's the point anyway you ask. Good question. I don't
+know. I only hope I never decide to switch away from Hetzner.
+
+Peace.
diff --git a/export_base_epub/container.xml b/export_base_epub/container.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8b80cd0770c07481152c8336a6fa5133559b4e25
--- /dev/null
+++ b/export_base_epub/container.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/export_base_epub/content.opf b/export_base_epub/content.opf
new file mode 100644
index 0000000000000000000000000000000000000000..c87525e67cf8a698b6238a8af383012e0318646d
--- /dev/null
+++ b/export_base_epub/content.opf
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/export_base_epub/mimetype b/export_base_epub/mimetype
new file mode 100644
index 0000000000000000000000000000000000000000..403c4f02dfa7f008d60eee9539a2695157623370
--- /dev/null
+++ b/export_base_epub/mimetype
@@ -0,0 +1 @@
+application/epub+zip
diff --git a/export_base_epub/toc.ncx b/export_base_epub/toc.ncx
new file mode 100644
index 0000000000000000000000000000000000000000..68114b119a85c3859a5ca6970c36fb97a802173c
--- /dev/null
+++ b/export_base_epub/toc.ncx
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title page
+
+
+
+
+ Table of Contents
+
+
+
+
+
diff --git a/export_base_epub/toc.xhtml b/export_base_epub/toc.xhtml
new file mode 100644
index 0000000000000000000000000000000000000000..6d4e6242ce949764ee4eed755c8b87b0fae8f72a
--- /dev/null
+++ b/export_base_epub/toc.xhtml
@@ -0,0 +1,15 @@
+
+
+
+
+ Table of Contents
+
+
+
+
+
+{% endblock content %}
diff --git a/export_base_zola/_index.md b/export_base_zola/_index.md
new file mode 100644
index 0000000000000000000000000000000000000000..8bc00690ea34d5db98d6c0328ae89be8709eb1eb
--- /dev/null
+++ b/export_base_zola/_index.md
@@ -0,0 +1,3 @@
++++
+sort_by = "date"
++++
diff --git a/export_base_zola/config.toml b/export_base_zola/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b8c4bdd4765170ceb9a9dda1796641c3425a9bcf
--- /dev/null
+++ b/export_base_zola/config.toml
@@ -0,0 +1,22 @@
+# The URL the site will be built for
+base_url = "https://example.com"
+
+title = "Example blog title"
+description = "Example blog description"
+
+# Whether to automatically compile all Sass files in the sass directory
+compile_sass = false
+
+generate_feeds = true
+feed_filenames = ["rss.xml"]
+
+# Whether to do syntax highlighting
+# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
+[markdown]
+highlight_code = true
+
+# Whether to build a search index to be used later on by a JavaScript library
+build_search_index = false
+
+[extra]
+# Put all your custom variables here
diff --git a/export_base_zola/index.html b/export_base_zola/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e1996a4c439074ef3ea4678e7cf8ced55ea599cc
--- /dev/null
+++ b/export_base_zola/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ {% block title %}{{ config.title }}{% endblock title %}
+
+
+
+ {% block content %}
+
+
{{ config.title }}
+
+ {% if config.description %}
+
+ {{ config.description }}
+
+ {% endif %}
+
+
+ {% set blog = get_section(path='_index.md') %}
+ {% for post in blog.pages %}
+
+
+ {# grandfather case #}
+ {% if request.user.is_grandfathered %}
+ Currently and forever on the
+ Grandfather Plan.
+ Rejoice eternally.
+ {% endif %}
+
+ {# monero case #}
+ {% if not request.user.is_grandfathered and request.user.monero_address %}
+ {% if request.user.is_premium %}
+
+ Currently on Premium Plan.
+
+
+ Our Premium Plan costs 0.05 XMR per year and includes all features including
+ the ability to set a custom domain.
+
+ Your card details will be sent to Stripe (payment processor).
+
+
+ We will charge your card immediately, through Stripe.
+
+
+ We will store your card within Stripe, so that we can charge you
+ next year.
+
+
+ You will be able to cancel your subscription immediately as well,
+ should you want to. You can cancel your subscription at any point,
+ 24/7/365, from this billing dashboard with one click.
+
+
+ You can also get a refund, no questions asked.
+
Hugo: reliable and very popular static site generators
+
+
+ {% if not request.user.is_authenticated %}
+
Redirect
+ {% endif %}
+
+ If you are retiring your BōcPress blog, we can also
+ redirect to your new domain.
+ You can configure this in the bottom last field of your
+ blog settings.
+
+ Renders all blog posts in one page. Useful for exporting the blog into
+ PDF, or printing in dead trees form, or anything that might require bulk
+ copying.
+
+ {% if p.published_at and p.is_published %}
+ Published on
+ {% elif p.published_at and not p.is_published %}
+ SCHEDULED for
+ {% else %}
+ DRAFT — Last updated on
+ {% endif %}
+
+ We offer custom domains with managed TLS (https) to users who pay for the
+ Premium Plan.
+
+
+
Set up
+
+ Add an A record in your domain's DNS settings with IP:
+
+
+ 95.217.30.133
+
+
+
Notes
+
+ We only support one domain. This means that we cannot
+ auto-redirect a naked domain (e.g. example.com) to its www version (i.e.
+ www.example.com). To do this, you would need to choose one version and then
+ host/serve/create some kind of redirect mechanism for the other version.
+
+
+ If you use Cloudflare, take care to have "Proxied" turned
+ off. This means that the setting should be in mode "DNS Only".
+
+
+ Other things to try: dig, online DNS lookup tools, private/incognito mode,
+ different browser, different computer/phone.
+
+ Even though we are very text focused, we do support image uploading and hosting.
+ The limits are:
+
+
+
Max file size is 1MB
+
Total hosting up to 100MB
+
Total image count up to 1000
+
Bandwidth 100GB per year
+
+
+
Upload
+
+ There are two ways to upload an image. Either via the
+ image dashboard, or via any post or page editing page.
+
+
Image dashboard: To upload, use the file selector at the top.
+
+ Post/page edit: To upload, just drag and drop onto the text area of the new post or page.
+
+
+
Show images
+
+ To show the uploaded images on a BōcPress blog post or page, one can write the following:
+ 
+
+
+ This, of course, works with any image on the web:
+ 
+
+ BōcPress supports Markdown in the blog posts body. Markdown is a very
+ simple formatting language.
+
+
+
Italics
+
+ One can use *asterisks* between words for italics.
+
+
+
Bold
+
+ One can use **double asterisks* for bold letters.
+
+
+
Image
+
+
+
+
+ To display an image (such as the above dot), write:
+ 
+
+
+
Links
+
+ Write a link like this: [website link](https://sourcehut.org/)
+ for it to appear like this website link.
+
+
+
Headings
+
+ Headings on markdown are defined in a hierarchy of 6 levels. Heading level 1
+ is the main title, heading level 2 a secondary, under level 1, heading 3
+ tertiary under level 2, et al.
+
+
+ Each level is defined with the number of hash signs as prefixes. Eg:
+
+ # Markdown Guide
+ or
+ ## Italics.
+
+
+
Monospace font
+
+ For a monospace font `use backticks`; it will appear like this.
+
+
+
Line breaks
+
+ To change lines without changing paragraphs, one can add two spaces at the end of
+ the line, and then continue below. Example:
+
0:00<space><space>
+This is the second line
+
+
+
Footnotes
+
+ To add a footnote, use the following notation:
+
+
+ 1. On the main text, just after the word you want to have the footnote
+ superscript, add a word as reference, eg: [^picaso]
+
+
+ 2. At the end of the post, add the footnote content, on its own line,
+ as such:
+
+ [^picaso]: Pablo Ruiz Picasso was born on October 25th, 1881.
+
+
+ When saved, this will automatically create the numbering and make them
+ links. NB: the square brackets, the caret, and the colon are important
+ parts of the syntax.
+
+
+
In-page links
+
+ Every heading has an anchor automatically attached to it. For example:
+
+ ## Footnotes is #footnotes
+
+ ## Table of Contents is #table-of-contents
+
+
+ One can create markdown links with the anchors as targets. Eg:
+ [Bold](#bold) will become
+ Bold. This enables jumping between parts of
+ a single post and can be used to create a table of contents as well.
+
BōcPress was chosen because it blends heritage, literature, and modern publishing in a single, memorable name.
+
+
Let's break it down:
+
+
“Bōc” comes from the Old English word for book, evoking centuries of literary tradition and the enduring
+ power of the written word.
+
“Press” signals publishing and dissemination, connecting the platform to both historic printing presses and
+ today’s digital tools.
+
+
+
Together, BōcPress represents a modern platform for writers while honoring the timeless craft of storytelling.
+
+
+
This name reflects our mission: to provide a digital space where stories, ideas, and voices endure, combining
+ elegance, literary heritage, and modern accessibility.
+
+
FAQ
+
+
+ Do you support custom CSS?
+
+
+ Not at the moment. The philosophy of BōcPress is to be as minimal as possible without enabling
+ too many decisions about styling. We understand that this is not what some prefer and are open to ideas.
+
+
+
+
+
+ Can I self-host BōcPress?
+
+
+ Absolutely. The project is
+ open source
+ and you can find deployment documentation
+ in the docs.
+
+
+
+
+
+ Why should I use BōcPress instead of Substack/WordPress/etc?
+
+
+ BōcPress embraces a minimalist ethos: swift, unadorned, and focused on the heart of your words.
+ It values simplicity over bells and whistles, while offering strong data interoperability.
+ Your content is yours to keep, and exporting it is effortless.
+
+
+
+
+
+ Will you add themes?
+
+
+ At present, there are no plans to add custom themes or templates. It's
+ part of our philosophy to enable one to just write and publish without
+ having to make too many decisions about styling. We understand that this is not what some prefer and are open to ideas.
+
+
+ Nonetheless, there are discussions around adding some minor styling decisions for authors,
+ but these are unplanned at the moment. Feel free to get in touch with any ideas you may have around this topic.
+
+
+
+
+
+ What about tags or categories?
+
+
+ These are not currently supported, however there are plans to add them. We want to keep the platform simple,
+ but understand that tags or categories can help with organization and discovery. If implemented,
+ they will be designed to remain minimal and unobtrusive.
+
+
+
+
+
+ What about pagination?
+
+
+ Pagination can be difficult as it breaks the browser search across all post titles.
+ Additionally, because of how fast and simple BōcPress is, speed is not an issue
+ even with thousands of posts. This is a feature we may consider in the future, but
+ it is not currently planned.
+
+ The markdown library we use
+ (Python-Markdown)
+ supports
+
+ John Gruber’s markdown spec,
+ which does not define strikethrough text. Discussions have been had on migrating to another
+ markdown library at some point in the future, though, this is not
+ yet planned.
+
+
+
+
+
+ Would you consider a rich text editor option?
+
+
+ At present, a rich text editor is too much for BōcPress. Something like Trix does look like it would be great,
+ however the added complexity is too high with 200K of JavaScript code. Saving HTML in the database instead of
+ simple text is also less than ideal.
+
+
+
+
+
+ Would you add support for LaTeX-style math expressions?
+
+
+ This was recently introduced in V1.3.1. Please see the pull request for more information or give it a try yourself.
+
+
+
+
+
+ What about supporting Mermaid flowcharts?
+
+
+ We would not add
+ mermaid-js
+ as it is
+
+ 2.7MB minified
+
+ JavaScript which is far too heavy for BōcPress' mission. However, we would be interested
+ in implementing something similar if it was rendered on the backend.
+
+
+
+
+
+ Do you support ActivityPub Federation?
+
+
+ We do not, however, we'd like to. This is unplanned work that might
+ happen at some point in the future.
+
+ mataroa.blog exists to enable people to have their own voice on the web
+ without needing to rely on the platforms and infrastructure of the
+ most powerful.
+
+
+ We want to do that by empowering personal independent blogs.
+
+
+
Ethics
+
We are committed to:
+
+
No tracking of user or visitor behaviour.
+
Never sell any user or visitor data.
+
No ads — ever.
+
+
+
Code of Publication
+
+ Mataroa is designed to be a place for people to voice their thoughts.
+
+
+ However, we do not want to provide a platform for thoughts that
+ are spiteful or malevolent to an individual or a group on account of
+ their race, colour, nationality, sex, disability, religion, or sexual
+ orientation.
+
+
+ Additionally, we do not want to contribute to the current state of the
+ web, which is ridden with ads, SEO tricks, and bot content. This
+ includes blogs with extremely low quality content that is designed to
+ serve as marketing for a specific shop or professional. Eg: a blog
+ named "Dentist in London" which contains a post titled "How to find the
+ best dentist", which itself contains a few paragraphs of random advice
+ and a link to one specific dentist in London.
+
+
+ Any blogs found that match the above descriptions will be immediately
+ deleted, with an final markdown export emailed to the blog author (in
+ case they have an email on their account), and a notice of why their
+ blog was deleted.
+
+
+
Comments Moderation
+
+ Comments in mataroa are filtered and reviewed by blog authors.
+
+
+
Business Transparency
+
+ We aim to be as transparent as possible. We maintain a
+ Business Transparency
+ page with data on our revenue and costs.
+
+
+
Account Terms
+
+
+ The user is responsible for all content posted and all actions
+ performed with their account.
+
+
+ The user is responsible for maintaining the security of their
+ account and password.
+
+
+ We reserve the right to disable or delete a user's account for any
+ reason at any time. We have this clause because, statistically
+ speaking, there will be people trying to do something nefarious.
+
+
+ We do not require an email address to register an account. However,
+ it is the only way for us to contact a user in cases of any
+ service update or account access restoration. For this reason,
+ having an email registered is very useful.
+
+
+
+
Account Data
+
+ In order to have a functional mataroa.blog account a username and a
+ password are required. An email is also asked as it is the only way for
+ a user to restore their account in case of a forgotten password.
+ However, an email is not required.
+
+
+ A user is able to change their username and password and any other
+ details (eg. email) through their
+ dashboard.
+
+
+ A user is able to export all their data directly and at any point
+ through the export page.
+
+
+ A user is able to completely delete their account and all information
+ related to their account through the dashboard, and then navigating to
+ blog settings, and scrolling all
+ the way down. In this case, the user account will be immediately
+ purged from our primary servers and 20 days later from our database
+ backups.
+
+
+
Payments
+
+ We offer a Premium Plan of our service which requires payment. If a
+ user opts for the Premium Plan they are billed immediately for the
+ next one year term, and automatically billed every year unless cancelled.
+
+
+ We accept card payments through Stripe,
+ but if one is not fond of this method we support alternatives. Please,
+ email admin@mataroa.blog for
+ and with details.
+
+
+ We also fund CO₂ removal from the atmosphere using 5% of our
+ subscription revenue through
+ Stripe Climate.
+
+
+
Refunds
+
+ We wouldn’t want to cause unhappiness. Any dissatisfied with our
+ service user can ask—and most probably get—a refund at
+ admin@mataroa.blog.
+
+
+
Liability
+
+ The user expressly understands and agrees that Zermelo Fraenkel LTD,
+ the operators of this website shall not be liable, in law or in equity,
+ to them or to any third party for any direct, indirect, incidental,
+ lost profits, special, consequential, punitive or exemplary damages.
+
+
+
Third-parties
+
+ We have a strong commitment to never share any user data with any
+ third-parties. The only neccessary exception to this rule is the
+ payment processor we use to accept card payments. That processor is
+ Stripe and the data sent over are
+ card numbers. This enables us to never—not even temporarily—store card
+ details on our servers and benefit from Stripe’s secure,
+ PCI-compliant
+ payment infrastructure.
+
+
+
+ Please bear in mind that Stripe may also collect other data, such as IP
+ address and browser user agent.
+
+
+
Service Availability
+
+ We provide the mataroa.blog service on an “as is” and “as available”
+ basis. We do not offer service-level agreements—but do take uptime
+ seriously. You can find a record of outages at
+ status.mataroa.blog.
+
+ and of course the creators and contributors of Python, Ubuntu, Debian, Linux kernel,
+ Bash, GNU Project, rclone, Let’s Encrypt, C, Git, vim, and the list is never ending...
+
+
+
+
Changes
+
+ Maybe we’ll change our minds for some of these statements. In cases of
+ major changes, users with an email to their account will receive a
+ notice 14 days prior.
+
+ Who:
+ For the current list of registered emails, see
+ Subscribers.
+
+
+ When:
+ Every day at 10:00 UTC all posts published the day before are emailed to
+ subscribers.
+
+
+ What:
+ Newsletter emails are delivered in raw plain text (markdown source) as seen in
+ the editor. Images are included as links only.
+
+
+
+ Past newsletters:
+
+
+ {% regroup notificationrecord_list_sent by post as post_list %}
+ {% for post, notificationrecord_list in post_list %}
+
+ {{ post.title }}
+
+ sent at
+
+ to these {{ notificationrecord_list|length }} subscribers:
+
+ {% for nr in notificationrecord_list %}
+ {{ nr.notification.email }}
+ {% endfor %}
+
+ {% if post.published_at and post.is_published %}
+ Published on
+ {% elif post.published_at and not post.is_published %}
+ SCHEDULED for
+ {% else %}
+ DRAFT — Last updated on
+ {% endif %}
+ {% if request.user.is_authenticated and request.subdomain == request.user.username %}
+ | Edit post
+ | Delete post
+ {% endif %}
+
+ {% if post.is_draft %}
+
+
+ ⚠️ This post is accessible to anyone with the link!
+
+
+ {% endif %}
+
+
+ {% if blog_user.reading_time_on %}
+
+ Estimated reading time: {{ reading_time }} min
+
+ {% endif %}
+
+
+ {{ post.body_as_html|safe }}
+
+
+
+
+{% include 'partials/webring.html' %}
+
+{% if blog_user.comments_on %}
+
+ {% if request.user.is_authenticated and request.subdomain == request.user.username %}
+
Post comment as blog author
+
+
+
+
+ {% else %}
+
+
Thoughts? Leave a comment
+
+
+
+ {% endif %}
+
+ {% if comments_pending and request.user.is_authenticated and request.subdomain == request.user.username %}
+
Really Simple Licensing (RSL) builds on the foundational ideas of the RSS standard, which allowed publishers to syndicate content in a machine-readable format for third-party clients and crawlers, often in exchange for increased traffic.
+
+
+ RSL expands these concepts by incorporating explicit licensing terms, letting publishers specify machine-readable conditions for content use and compensation. The RSL Technical Steering Committee guides the development of the standard in partnership with publishers, technology companies, industry groups, and other stakeholders.
+
+
+
+ BōcPress supports the RSL standard by allowing you to add your license text (XML) and choose where to display it.
+
+ These are the latest snapshots automatically saved, across all posts.
+ Post backups are saved automatically, every few seconds, from the post
+ create and post edit pages.
+
+ Once upon a time, the web was born. One of the early residents of the web
+ was the so-called weblog. The weblog — or just blog — was a log on the
+ web. A log as in a ship's log: a record of important events in a ship's history.
+
+
+ A blog is a mode of publication; as is the book, the magazine, the newspaper,
+ even the television show. We see the blog as a first-class citizen in online
+ publishing. The simplest form to express and distribute ideas. The simplest
+ model for just saying something on the internet.
+
+
+ Originally, a blog was an online diary or a list of updates for an online
+ product. Today, a blog is a newsletter subscription, or a company's newsroom, or
+ anything that anybody writes in the online medium.
+
+
+ We, the internet users, now have an amazing selection of publishing and
+ distribution options. From tweets to articles to books, from text to image to videos,
+ from free software to walled gardens, from simple email delivery to personalised
+ algorithms, from bare text to fully-featured online shops. The present is
+ bright. There is something for everyone and we hope BōcPress fulfills
+ the needs of those who want to just write.
+
+
+
+ We offer a place of no distractions to facilitate the most foundational mode
+ of intellectual internetic exchange: write a blog entry.
+
+
+
+
+
+
+ Read our Platform Methodology
+ for details on how we operate and how we protect our users’ privacy.
+
+ {% for comment in comments_pending %} +-
+
+
+ {{ comment.name|default_if_none:"Anonymous" }}
+ {% if comment.email %}
+ ({{ comment.email }})
+ {% endif %}—
+ {{ comment.created_at|date:'M j, Y' }}:
+
+
+ (approve
+ / delete)
+
+
{{ comment.body_as_html|safe }}
+
+ {% endfor %}
+
+ {% endif %} + + {% if comments %} + ++ {% for comment in comments %} +-
+
+ {% if comment.is_author %}
+
+ {{ comment.name }}
+ —
+ {{ comment.created_at|date:'M j, Y' }}:
+
+ {% else %}
+
+ {{ comment.name|default_if_none:"Anonymous" }}
+ {% if request.user.is_authenticated and request.subdomain == request.user.username and comment.email %}
+ ({{ comment.email }})
+ {% endif %}—
+ {{ comment.created_at|date:'M j, Y' }}:
+
+ {% endif %}
+
+ {% if request.user.is_authenticated and request.subdomain == request.user.username %}
+ (delete)
+ {% endif %}
+
+
{{ comment.body_as_html|safe }}
+
+ {% endfor %}
+
+ {% endif %} +