From 0227bd507514c735b9dd09e08c13388659bb63c2 Mon Sep 17 00:00:00 2001 From: cloudfreexiao <996442717qqcom@gmail.com> Date: Sun, 25 Jul 2021 13:25:40 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20build:=20=E8=B0=83=E6=95=B4=20we?= =?UTF-8?q?b=20=E5=90=8E=E7=AB=AF=20=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/backend/.busted | 12 + website/backend/.env.dev | 1 + website/backend/.env.prod | 1 + website/backend/.env.test | 1 + website/backend/.gitattributes | 2 + website/backend/.gitignore | 15 + website/backend/LICENSE.md | 19 + website/backend/README.md | 35 + website/backend/app/Dockerfile | 69 + website/backend/app/app.lua | 46 + website/backend/app/config.lua | 68 + website/backend/app/docker-entrypoint.sh | 22 + website/backend/app/migrations.lua | 148 ++ website/backend/app/mime.types | 79 + website/backend/app/nginx.conf | 60 + website/backend/app/spec/app_spec.lua | 17 + website/backend/app/src/apps/api.lua | 21 + .../app/src/apps/api/announcements.lua | 14 + .../apps/api/announcements/announcement.lua | 64 + .../apps/api/announcements/announcements.lua | 51 + .../app/src/apps/api/announcements/global.lua | 21 + website/backend/app/src/apps/api/bans.lua | 14 + website/backend/app/src/apps/api/bans/ban.lua | 70 + .../backend/app/src/apps/api/bans/bans.lua | 54 + .../backend/app/src/apps/api/bans/bans_ip.lua | 25 + website/backend/app/src/apps/api/boards.lua | 23 + .../app/src/apps/api/boards/announcements.lua | 25 + .../app/src/apps/api/boards/archived.lua | 24 + .../backend/app/src/apps/api/boards/bans.lua | 29 + .../backend/app/src/apps/api/boards/board.lua | 114 + .../app/src/apps/api/boards/boards.lua | 79 + .../backend/app/src/apps/api/boards/post.lua | 92 + .../app/src/apps/api/boards/post_reports.lua | 21 + .../backend/app/src/apps/api/boards/posts.lua | 105 + .../app/src/apps/api/boards/reports.lua | 29 + .../app/src/apps/api/boards/thread.lua | 69 + .../src/apps/api/boards/thread_reports.lua | 21 + .../app/src/apps/api/boards/threads.lua | 35 + website/backend/app/src/apps/api/core.lua | 13 + .../backend/app/src/apps/api/core/login.lua | 35 + .../backend/app/src/apps/api/core/root.lua | 11 + .../app/src/apps/api/internal/action_base.lua | 17 + .../app/src/apps/api/internal/before_auth.lua | 41 + .../src/apps/api/internal/before_locale.lua | 20 + website/backend/app/src/apps/api/pages.lua | 13 + .../backend/app/src/apps/api/pages/page.lua | 60 + .../backend/app/src/apps/api/pages/pages.lua | 45 + website/backend/app/src/apps/api/users.lua | 13 + .../backend/app/src/apps/api/users/user.lua | 108 + .../backend/app/src/apps/api/users/users.lua | 64 + website/backend/app/src/apps/web.lua | 16 + website/backend/app/src/apps/web/admin.lua | 16 + .../app/src/apps/web/admin/announcement.lua | 128 ++ .../backend/app/src/apps/web/admin/board.lua | 136 ++ .../backend/app/src/apps/web/admin/index.lua | 138 ++ .../backend/app/src/apps/web/admin/page.lua | 164 ++ .../backend/app/src/apps/web/admin/report.lua | 64 + .../backend/app/src/apps/web/admin/user.lua | 190 ++ website/backend/app/src/apps/web/boards.lua | 14 + .../app/src/apps/web/boards/archive.lua | 76 + .../backend/app/src/apps/web/boards/board.lua | 242 +++ .../app/src/apps/web/boards/catalog.lua | 128 ++ .../app/src/apps/web/boards/thread.lua | 242 +++ .../app/src/apps/web/internal/check_auth.lua | 29 + .../app/src/apps/web/internal/check_ban.lua | 41 + .../app/src/apps/web/internal/code_404.lua | 14 + .../app/src/apps/web/internal/config_site.lua | 45 + .../app/src/apps/web/internal/install.lua | 333 +++ website/backend/app/src/apps/web/pages.lua | 14 + .../backend/app/src/apps/web/pages/index.lua | 10 + .../backend/app/src/apps/web/pages/logout.lua | 9 + .../backend/app/src/apps/web/pages/page.lua | 28 + .../backend/app/src/apps/web/pages/rules.lua | 19 + website/backend/app/src/locale/en.lua | 236 ++ website/backend/app/src/locale/fr.lua | 234 ++ website/backend/app/src/locale/phpceo.lua | 234 ++ website/backend/app/src/locale/pl.lua | 240 ++ website/backend/app/src/models.lua | 1 + .../backend/app/src/models/announcements.lua | 96 + website/backend/app/src/models/bans.lua | 151 ++ website/backend/app/src/models/boards.lua | 226 ++ website/backend/app/src/models/pages.lua | 90 + website/backend/app/src/models/posts.lua | 387 ++++ website/backend/app/src/models/reports.lua | 76 + website/backend/app/src/models/threads.lua | 106 + website/backend/app/src/models/users.lua | 181 ++ website/backend/app/src/sass/posts.scss | 109 + website/backend/app/src/sass/reset.scss | 141 ++ website/backend/app/src/sass/style.scss | 322 +++ website/backend/app/src/sass/yotsuba.scss | 113 + website/backend/app/src/sass/yotsuba_b.scss | 109 + website/backend/app/src/utils/capture.lua | 35 + website/backend/app/src/utils/error.lua | 104 + .../backend/app/src/utils/file_whitelist.lua | 22 + website/backend/app/src/utils/generate.lua | 94 + .../app/src/utils/request_processor.lua | 298 +++ website/backend/app/src/utils/role.lua | 31 + .../backend/app/src/utils/text_formatter.lua | 160 ++ .../backend/app/src/views/admin/admin.etlua | 247 +++ .../app/src/views/admin/announcement.etlua | 30 + .../backend/app/src/views/admin/board.etlua | 189 ++ .../backend/app/src/views/admin/login.etlua | 21 + .../backend/app/src/views/admin/page.etlua | 33 + .../backend/app/src/views/admin/success.etlua | 3 + .../backend/app/src/views/admin/user.etlua | 71 + website/backend/app/src/views/archive.etlua | 49 + website/backend/app/src/views/banned.etlua | 8 + website/backend/app/src/views/board.etlua | 36 + website/backend/app/src/views/catalog.etlua | 34 + website/backend/app/src/views/code_404.etlua | 2 + .../src/views/fragments/announcements.etlua | 6 + .../app/src/views/fragments/board_title.etlua | 6 + .../app/src/views/fragments/copyright.etlua | 9 + .../app/src/views/fragments/error.etlua | 6 + .../app/src/views/fragments/form_ban.etlua | 15 + .../app/src/views/fragments/form_delete.etlua | 9 + .../app/src/views/fragments/form_locale.etlua | 10 + .../app/src/views/fragments/form_lock.etlua | 9 + .../src/views/fragments/form_override.etlua | 9 + .../app/src/views/fragments/form_remix.etlua | 9 + .../app/src/views/fragments/form_report.etlua | 6 + .../app/src/views/fragments/form_save.etlua | 9 + .../app/src/views/fragments/form_sticky.etlua | 9 + .../app/src/views/fragments/form_submit.etlua | 144 ++ .../app/src/views/fragments/list_boards.etlua | 16 + .../app/src/views/fragments/op_content.etlua | 71 + .../src/views/fragments/post_content.etlua | 39 + .../app/src/views/fragments/post_menu.etlua | 21 + .../src/views/fragments/return_board.etlua | 3 + .../src/views/fragments/return_thread.etlua | 3 + website/backend/app/src/views/index.etlua | 16 + website/backend/app/src/views/install.etlua | 132 ++ website/backend/app/src/views/layout.etlua | 30 + website/backend/app/src/views/page.etlua | 2 + website/backend/app/src/views/rules.etlua | 17 + website/backend/app/src/views/thread.etlua | 26 + website/backend/app/static/banned.jpg | Bin 0 -> 10148 bytes website/backend/app/static/css/posts.css | 63 + website/backend/app/static/css/posts.css.map | 7 + website/backend/app/static/css/reset.css | 200 ++ website/backend/app/static/css/reset.css.map | 7 + website/backend/app/static/css/style.css | 478 ++++ website/backend/app/static/css/style.css.map | 7 + website/backend/app/static/css/tegaki.css | 187 ++ website/backend/app/static/css/yotsuba.css | 63 + .../backend/app/static/css/yotsuba.css.map | 7 + website/backend/app/static/css/yotsuba_b.css | 61 + .../backend/app/static/css/yotsuba_b.css.map | 7 + website/backend/app/static/js/script.js | 249 +++ website/backend/app/static/js/tegaki/LICENSE | 19 + .../backend/app/static/js/tegaki/README.md | 10 + website/backend/app/static/js/tegaki/Rakefile | 20 + .../backend/app/static/js/tegaki/tegaki.js | 1925 +++++++++++++++++ website/backend/app/static/op_audio.png | Bin 0 -> 19745 bytes website/backend/app/static/op_spoiler.png | Bin 0 -> 35209 bytes website/backend/app/static/post_audio.png | Bin 0 -> 7635 bytes website/backend/app/static/post_spoiler.png | Bin 0 -> 18948 bytes website/backend/app/wait-for-it.sh | 177 ++ website/backend/data/backup/.keep | 0 website/backend/data/favicon.ico | Bin 0 -> 954 bytes website/backend/data/files/.keep | 0 website/backend/data/robots.txt | 2 + website/backend/data/secrets/.keep | 0 website/backend/dev.sh | 3 + website/backend/docker-compose.yml | 34 + website/backend/prod.sh | 3 + website/backend/psql/1-init.sql | 5 + website/backend/psql/Dockerfile | 2 + website/backend/psql/pg_hba.conf | 15 + website/backend/resources/op_spoiler.psd | Bin 0 -> 255845 bytes website/backend/resources/post_spoiler.psd | Bin 0 -> 128434 bytes website/backend/sass.sh | 3 + website/backend/test.sh | 3 + website/backend/ts.sh | 3 + website/backend/tsconfig.json | 13 + 175 files changed, 12855 insertions(+) create mode 100755 website/backend/.busted create mode 100755 website/backend/.env.dev create mode 100755 website/backend/.env.prod create mode 100755 website/backend/.env.test create mode 100755 website/backend/.gitattributes create mode 100755 website/backend/.gitignore create mode 100755 website/backend/LICENSE.md create mode 100755 website/backend/README.md create mode 100755 website/backend/app/Dockerfile create mode 100755 website/backend/app/app.lua create mode 100755 website/backend/app/config.lua create mode 100755 website/backend/app/docker-entrypoint.sh create mode 100755 website/backend/app/migrations.lua create mode 100755 website/backend/app/mime.types create mode 100755 website/backend/app/nginx.conf create mode 100755 website/backend/app/spec/app_spec.lua create mode 100755 website/backend/app/src/apps/api.lua create mode 100755 website/backend/app/src/apps/api/announcements.lua create mode 100755 website/backend/app/src/apps/api/announcements/announcement.lua create mode 100755 website/backend/app/src/apps/api/announcements/announcements.lua create mode 100755 website/backend/app/src/apps/api/announcements/global.lua create mode 100755 website/backend/app/src/apps/api/bans.lua create mode 100755 website/backend/app/src/apps/api/bans/ban.lua create mode 100755 website/backend/app/src/apps/api/bans/bans.lua create mode 100755 website/backend/app/src/apps/api/bans/bans_ip.lua create mode 100755 website/backend/app/src/apps/api/boards.lua create mode 100755 website/backend/app/src/apps/api/boards/announcements.lua create mode 100755 website/backend/app/src/apps/api/boards/archived.lua create mode 100755 website/backend/app/src/apps/api/boards/bans.lua create mode 100755 website/backend/app/src/apps/api/boards/board.lua create mode 100755 website/backend/app/src/apps/api/boards/boards.lua create mode 100755 website/backend/app/src/apps/api/boards/post.lua create mode 100755 website/backend/app/src/apps/api/boards/post_reports.lua create mode 100755 website/backend/app/src/apps/api/boards/posts.lua create mode 100755 website/backend/app/src/apps/api/boards/reports.lua create mode 100755 website/backend/app/src/apps/api/boards/thread.lua create mode 100755 website/backend/app/src/apps/api/boards/thread_reports.lua create mode 100755 website/backend/app/src/apps/api/boards/threads.lua create mode 100755 website/backend/app/src/apps/api/core.lua create mode 100755 website/backend/app/src/apps/api/core/login.lua create mode 100755 website/backend/app/src/apps/api/core/root.lua create mode 100755 website/backend/app/src/apps/api/internal/action_base.lua create mode 100755 website/backend/app/src/apps/api/internal/before_auth.lua create mode 100755 website/backend/app/src/apps/api/internal/before_locale.lua create mode 100755 website/backend/app/src/apps/api/pages.lua create mode 100755 website/backend/app/src/apps/api/pages/page.lua create mode 100755 website/backend/app/src/apps/api/pages/pages.lua create mode 100755 website/backend/app/src/apps/api/users.lua create mode 100755 website/backend/app/src/apps/api/users/user.lua create mode 100755 website/backend/app/src/apps/api/users/users.lua create mode 100755 website/backend/app/src/apps/web.lua create mode 100755 website/backend/app/src/apps/web/admin.lua create mode 100755 website/backend/app/src/apps/web/admin/announcement.lua create mode 100755 website/backend/app/src/apps/web/admin/board.lua create mode 100755 website/backend/app/src/apps/web/admin/index.lua create mode 100755 website/backend/app/src/apps/web/admin/page.lua create mode 100755 website/backend/app/src/apps/web/admin/report.lua create mode 100755 website/backend/app/src/apps/web/admin/user.lua create mode 100755 website/backend/app/src/apps/web/boards.lua create mode 100755 website/backend/app/src/apps/web/boards/archive.lua create mode 100755 website/backend/app/src/apps/web/boards/board.lua create mode 100755 website/backend/app/src/apps/web/boards/catalog.lua create mode 100755 website/backend/app/src/apps/web/boards/thread.lua create mode 100755 website/backend/app/src/apps/web/internal/check_auth.lua create mode 100755 website/backend/app/src/apps/web/internal/check_ban.lua create mode 100755 website/backend/app/src/apps/web/internal/code_404.lua create mode 100755 website/backend/app/src/apps/web/internal/config_site.lua create mode 100755 website/backend/app/src/apps/web/internal/install.lua create mode 100755 website/backend/app/src/apps/web/pages.lua create mode 100755 website/backend/app/src/apps/web/pages/index.lua create mode 100755 website/backend/app/src/apps/web/pages/logout.lua create mode 100755 website/backend/app/src/apps/web/pages/page.lua create mode 100755 website/backend/app/src/apps/web/pages/rules.lua create mode 100755 website/backend/app/src/locale/en.lua create mode 100755 website/backend/app/src/locale/fr.lua create mode 100755 website/backend/app/src/locale/phpceo.lua create mode 100755 website/backend/app/src/locale/pl.lua create mode 100755 website/backend/app/src/models.lua create mode 100755 website/backend/app/src/models/announcements.lua create mode 100755 website/backend/app/src/models/bans.lua create mode 100755 website/backend/app/src/models/boards.lua create mode 100755 website/backend/app/src/models/pages.lua create mode 100755 website/backend/app/src/models/posts.lua create mode 100755 website/backend/app/src/models/reports.lua create mode 100755 website/backend/app/src/models/threads.lua create mode 100755 website/backend/app/src/models/users.lua create mode 100755 website/backend/app/src/sass/posts.scss create mode 100755 website/backend/app/src/sass/reset.scss create mode 100755 website/backend/app/src/sass/style.scss create mode 100755 website/backend/app/src/sass/yotsuba.scss create mode 100755 website/backend/app/src/sass/yotsuba_b.scss create mode 100755 website/backend/app/src/utils/capture.lua create mode 100755 website/backend/app/src/utils/error.lua create mode 100755 website/backend/app/src/utils/file_whitelist.lua create mode 100755 website/backend/app/src/utils/generate.lua create mode 100755 website/backend/app/src/utils/request_processor.lua create mode 100755 website/backend/app/src/utils/role.lua create mode 100755 website/backend/app/src/utils/text_formatter.lua create mode 100755 website/backend/app/src/views/admin/admin.etlua create mode 100755 website/backend/app/src/views/admin/announcement.etlua create mode 100755 website/backend/app/src/views/admin/board.etlua create mode 100755 website/backend/app/src/views/admin/login.etlua create mode 100755 website/backend/app/src/views/admin/page.etlua create mode 100755 website/backend/app/src/views/admin/success.etlua create mode 100755 website/backend/app/src/views/admin/user.etlua create mode 100755 website/backend/app/src/views/archive.etlua create mode 100755 website/backend/app/src/views/banned.etlua create mode 100755 website/backend/app/src/views/board.etlua create mode 100755 website/backend/app/src/views/catalog.etlua create mode 100755 website/backend/app/src/views/code_404.etlua create mode 100755 website/backend/app/src/views/fragments/announcements.etlua create mode 100755 website/backend/app/src/views/fragments/board_title.etlua create mode 100755 website/backend/app/src/views/fragments/copyright.etlua create mode 100755 website/backend/app/src/views/fragments/error.etlua create mode 100755 website/backend/app/src/views/fragments/form_ban.etlua create mode 100755 website/backend/app/src/views/fragments/form_delete.etlua create mode 100755 website/backend/app/src/views/fragments/form_locale.etlua create mode 100755 website/backend/app/src/views/fragments/form_lock.etlua create mode 100755 website/backend/app/src/views/fragments/form_override.etlua create mode 100755 website/backend/app/src/views/fragments/form_remix.etlua create mode 100755 website/backend/app/src/views/fragments/form_report.etlua create mode 100755 website/backend/app/src/views/fragments/form_save.etlua create mode 100755 website/backend/app/src/views/fragments/form_sticky.etlua create mode 100755 website/backend/app/src/views/fragments/form_submit.etlua create mode 100755 website/backend/app/src/views/fragments/list_boards.etlua create mode 100755 website/backend/app/src/views/fragments/op_content.etlua create mode 100755 website/backend/app/src/views/fragments/post_content.etlua create mode 100755 website/backend/app/src/views/fragments/post_menu.etlua create mode 100755 website/backend/app/src/views/fragments/return_board.etlua create mode 100755 website/backend/app/src/views/fragments/return_thread.etlua create mode 100755 website/backend/app/src/views/index.etlua create mode 100755 website/backend/app/src/views/install.etlua create mode 100755 website/backend/app/src/views/layout.etlua create mode 100755 website/backend/app/src/views/page.etlua create mode 100755 website/backend/app/src/views/rules.etlua create mode 100755 website/backend/app/src/views/thread.etlua create mode 100755 website/backend/app/static/banned.jpg create mode 100755 website/backend/app/static/css/posts.css create mode 100755 website/backend/app/static/css/posts.css.map create mode 100755 website/backend/app/static/css/reset.css create mode 100755 website/backend/app/static/css/reset.css.map create mode 100755 website/backend/app/static/css/style.css create mode 100755 website/backend/app/static/css/style.css.map create mode 100755 website/backend/app/static/css/tegaki.css create mode 100755 website/backend/app/static/css/yotsuba.css create mode 100755 website/backend/app/static/css/yotsuba.css.map create mode 100755 website/backend/app/static/css/yotsuba_b.css create mode 100755 website/backend/app/static/css/yotsuba_b.css.map create mode 100755 website/backend/app/static/js/script.js create mode 100755 website/backend/app/static/js/tegaki/LICENSE create mode 100755 website/backend/app/static/js/tegaki/README.md create mode 100755 website/backend/app/static/js/tegaki/Rakefile create mode 100755 website/backend/app/static/js/tegaki/tegaki.js create mode 100755 website/backend/app/static/op_audio.png create mode 100755 website/backend/app/static/op_spoiler.png create mode 100755 website/backend/app/static/post_audio.png create mode 100755 website/backend/app/static/post_spoiler.png create mode 100755 website/backend/app/wait-for-it.sh create mode 100755 website/backend/data/backup/.keep create mode 100755 website/backend/data/favicon.ico create mode 100755 website/backend/data/files/.keep create mode 100755 website/backend/data/robots.txt create mode 100755 website/backend/data/secrets/.keep create mode 100755 website/backend/dev.sh create mode 100755 website/backend/docker-compose.yml create mode 100755 website/backend/prod.sh create mode 100755 website/backend/psql/1-init.sql create mode 100755 website/backend/psql/Dockerfile create mode 100755 website/backend/psql/pg_hba.conf create mode 100755 website/backend/resources/op_spoiler.psd create mode 100755 website/backend/resources/post_spoiler.psd create mode 100755 website/backend/sass.sh create mode 100755 website/backend/test.sh create mode 100755 website/backend/ts.sh create mode 100755 website/backend/tsconfig.json diff --git a/website/backend/.busted b/website/backend/.busted new file mode 100755 index 0000000..bc2ea5d --- /dev/null +++ b/website/backend/.busted @@ -0,0 +1,12 @@ +package.path = "./src/?.lua;./src/?/init.lua;" .. package.path +package.cpath = "" .. package.cpath + +return { + _all = { + coverage = true + }, + + default = { + + } +} diff --git a/website/backend/.env.dev b/website/backend/.env.dev new file mode 100755 index 0000000..4ff3f75 --- /dev/null +++ b/website/backend/.env.dev @@ -0,0 +1 @@ +LAPIS_CONFIG=development diff --git a/website/backend/.env.prod b/website/backend/.env.prod new file mode 100755 index 0000000..7cd3637 --- /dev/null +++ b/website/backend/.env.prod @@ -0,0 +1 @@ +LAPIS_CONFIG=production diff --git a/website/backend/.env.test b/website/backend/.env.test new file mode 100755 index 0000000..28bcca9 --- /dev/null +++ b/website/backend/.env.test @@ -0,0 +1 @@ +LAPIS_CONFIG=test diff --git a/website/backend/.gitattributes b/website/backend/.gitattributes new file mode 100755 index 0000000..dfe0770 --- /dev/null +++ b/website/backend/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/website/backend/.gitignore b/website/backend/.gitignore new file mode 100755 index 0000000..136000d --- /dev/null +++ b/website/backend/.gitignore @@ -0,0 +1,15 @@ +# Directories +*_temp +.sass-cache +data/backup/* +data/files/* +data/secrets/* +logs + +# Files +*.*~ +*.compiled +*.out + +# Unignore Files +!.keep diff --git a/website/backend/LICENSE.md b/website/backend/LICENSE.md new file mode 100755 index 0000000..4b44ad7 --- /dev/null +++ b/website/backend/LICENSE.md @@ -0,0 +1,19 @@ +# The MIT/X11 License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/website/backend/README.md b/website/backend/README.md new file mode 100755 index 0000000..a45fea5 --- /dev/null +++ b/website/backend/README.md @@ -0,0 +1,35 @@ +# Lapis-chan + +Lapis-chan is a text and image board written in Lua using the Lapis web framework. + +# Features + +To view a complete list of features, check out the [Feature Set](https://docs.google.com/spreadsheets/d/19WfJm5cT_QHkuStD4NbuWLZ8EEhr23yEmJbS083mjQE/edit?usp=sharing) spreadsheet. + +# Install + +# Installing + +``` +$ docker-compose build +``` + +## Create Cryptographic Secrets + +In the `secrets` directory, open up both the `token.lua` and `salt.lua` files. + +### Secret Token + +The secret token should be a random string of characters between 40 and 60 characters in length. Change `CHANGE_ME` to your secret token. Keep this token extremely safe, it is the backbone of security on Lapis-chan! Don't lose it, either! + +### Secret Salt + +The secret salt should be a random string of characters exactly two characters in length. The salt can be comprised of letters, numbers, a period (".") or a slash ("\\"). Change `CHANGE_ME` to your secret salt. This salt is not necessarily meant to be secure, but don't hand it out willy-nilly either. This is only used for generating insecure tripcodes. + +## Start Lapis + +Now we're ready to finish the installation! + +``` +$ prod.sh +``` diff --git a/website/backend/app/Dockerfile b/website/backend/app/Dockerfile new file mode 100755 index 0000000..0908da5 --- /dev/null +++ b/website/backend/app/Dockerfile @@ -0,0 +1,69 @@ +FROM openresty/openresty:centos-rpm + +LABEL maintainer="Landon Manning " + +ARG GIFLIB_VERSION="5.1.1" +ARG OPENSSL_DIR="/usr/local/openresty/openssl" + +# Install repos +RUN yum -y install --nogpgcheck \ + https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm \ + https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm \ + https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-8.noarch.rpm \ + && yum config-manager --set-enabled powertools + +# Install from repos +RUN yum -y install \ + dos2unix \ + ffmpeg \ + gcc \ + git \ + ImageMagick-devel \ + openresty-openssl-devel \ + openssl-devel \ + which \ + ; yum clean all + +# Install giflib +RUN cd /tmp \ + && curl -fSL "https://downloads.sourceforge.net/project/giflib/giflib-${GIFLIB_VERSION}.tar.gz" -o giflib-${GIFLIB_VERSION}.tar.gz \ + && tar xzf giflib-${GIFLIB_VERSION}.tar.gz \ + && cd giflib-${GIFLIB_VERSION} \ + && ./configure \ + --prefix=/usr \ + && make \ + && make install \ + && cd / \ + && rm -rf /tmp/* + +# Install from LuaRocks +RUN luarocks install luasec \ + && luarocks install bcrypt \ + && luarocks install busted \ + && luarocks install giflib \ + --server=http://luarocks.org/dev \ + && luarocks install i18n \ + && luarocks install lapis \ + CRYPTO_DIR=${OPENSSL_DIR} \ + CRYPTO_INCDIR=${OPENSSL_DIR}/include \ + OPENSSL_DIR=${OPENSSL_DIR} \ + OPENSSL_INCDIR=${OPENSSL_DIR}/include \ + && luarocks install lua-resty-jit-uuid \ + && luarocks install luacov \ + && luarocks install luaposix \ + && luarocks install magick \ + && luarocks install markdown \ + && luarocks install md5 + +# Wait for it +ADD wait-for-it.sh /usr/local/bin/wait-for-it.sh +RUN chmod +x /usr/local/bin/wait-for-it.sh \ + && dos2unix /usr/local/bin/wait-for-it.sh + +# Entrypoint +ADD docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh \ + && dos2unix /usr/local/bin/docker-entrypoint.sh + +# Update LD cache +RUN ldconfig diff --git a/website/backend/app/app.lua b/website/backend/app/app.lua new file mode 100755 index 0000000..aadc808 --- /dev/null +++ b/website/backend/app/app.lua @@ -0,0 +1,46 @@ +local lapis = require "lapis" +local app = lapis.Application() +app.include = function(self, a) + self.__class.include(self, a, nil, self) +end + +app:enable "etlua" +app.layout = require "views.layout" + +do + function app.handle_404() + local api = _G.ngx.var.uri:match("^(/api).+$") + + if not api then + return { render="code_404" } + else + return { + status = 404, + json = { "Resource not found!" } -- FIXME: i18n + } + end + end +end + +-- NOTE: https://github.com/leafo/lapis/issues/706 +do + local super = app.__index.dispatch + app.__index.dispatch = function(self, req, res) + req.parsed_url.path = _G.ngx.var.uri + super(self, req, res) + end +end + +--[[ -- app:before_filter(require "apps.web.internal.install") -- FIXME: set up installer as a simple before filter +do + local r2 = require("lapis.application").respond_to + app:before_filter(require "apps.web.internal.config_site") + app:match("/", r2(require "apps.web.internal.install")) + return app +end +--]] + +app:include("apps.api") +app:include("apps.web") + +return app diff --git a/website/backend/app/config.lua b/website/backend/app/config.lua new file mode 100755 index 0000000..ec66c20 --- /dev/null +++ b/website/backend/app/config.lua @@ -0,0 +1,68 @@ +local config = require "lapis.config" +local secret = assert(loadfile("../data/secrets/token.lua"))() + +-- Use rewrite rules to create 'boards.' and 'static.' subdomains +-- Currently doesn't work, leave this as false! +local subdomains = false + +-- Maximum file size (update this in scripts.js too!) +local body_size = "15m" + +-- Maximum comment size (update this in scripts.js too!) +local text_size = 10000 + +-- Path to your lua libraries (LuaRocks and OpenResty) +local lua_path = "./src/?.lua;./src/?/init.lua" +local lua_cpath = "" + +config("development", { + site_name = "[DEVEL] Lapis-chan", + port = 80, + secret = secret, + subdomains = subdomains, + body_size = body_size, + text_size = text_size, + lua_path = lua_path, + lua_cpath = lua_cpath, + postgres = { + host = "psql", + user = "postgres", + password = "", + database = "lapischan" + }, +}) + +config("production", { + code_cache = "on", + site_name = "Lapis-chan", + port = 80, + secret = secret, + subdomains = subdomains, + body_size = body_size, + text_size = text_size, + lua_path = lua_path, + lua_cpath = lua_cpath, + postgres = { + host = "psql", + user = "postgres", + password = "", + database = "lapischan" + }, +}) + +config("test", { + site_name = "[DEVEL] Lapis-chan", + port = 80, + secret = secret, + subdomains = subdomains, + body_size = body_size, + text_size = text_size, + lua_path = lua_path, + lua_cpath = lua_cpath, + postgres = { + host = "psql", + user = "postgres", + password = "", + database = "lapischan_test" + }, +}) diff --git a/website/backend/app/docker-entrypoint.sh b/website/backend/app/docker-entrypoint.sh new file mode 100755 index 0000000..004f305 --- /dev/null +++ b/website/backend/app/docker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Wait for PostgreSQL +/usr/local/bin/wait-for-it.sh psql:5432 -s -q + +if [ $? -eq 0 ]; then + cd /var/www + + # Check if we are executing tests + if [ "$1" = "test" ]; then + lapis migrate $1 + busted + + # Run Application normally + else + lapis migrate + lapis server $1 + fi + +else + exit 5432 # PostgreSQL didn't load +fi diff --git a/website/backend/app/migrations.lua b/website/backend/app/migrations.lua new file mode 100755 index 0000000..ec856a7 --- /dev/null +++ b/website/backend/app/migrations.lua @@ -0,0 +1,148 @@ +local db = require "lapis.db" +local schema = require "lapis.db.schema" +local types = schema.types + +return { + [100] = function() + schema.create_table("users", { + { "id", types.serial { unique=true, primary_key=true }}, + { "username", types.varchar { unique=true }}, + { "password", types.varchar }, + { "admin", types.boolean { default=false }}, + { "mod", types.boolean { default=false }}, + { "janitor", types.boolean { default=false }} + }) + + schema.create_table("bans", { + { "id", types.serial { unique=true, primary_key=true }}, + { "board_id", types.integer { default=0 }}, + { "ip", types.varchar }, + { "reason", types.varchar { null=true }}, + { "time", types.integer }, + { "duration", types.integer { default=259200 }}, -- 3 days + }) + + schema.create_table("boards", { + { "id", types.serial { unique=true, primary_key=true }}, + { "short_name", types.varchar { unique=true }}, + { "name", types.varchar { unique=true }}, + { "subtext", types.varchar { null=true }}, + { "rules", types.text { null=true }}, + { "ban_message", types.varchar { default="USER WAS BANNED FOR THIS POST" }}, + { "anon_name", types.varchar { default="Anonymous" }}, + { "theme", types.varchar { default="yotsuba_b" }}, + { "posts", types.integer { default=0 }}, + { "pages", types.integer { default=10 }}, + { "threads_per_page", types.integer { default=10 }}, + { "text_only", types.boolean { default=false }}, + { "draw", types.boolean { default=false }}, + { "thread_file", types.boolean { default=true }}, + { "thread_comment", types.boolean { default=false }}, + { "thread_file_limit", types.integer { default=100 }}, + { "post_file", types.boolean { default=false }}, + { "post_comment", types.boolean { default=false }}, + { "post_limit", types.integer { default=250 }}, + { "archive", types.boolean { default=true }}, + { "archive_time", types.integer { default=2592000 }}, -- 30 days + { "group", types.integer { default=1 }} + }) + + schema.create_table("threads", { + { "id", types.serial { unique=true, primary_key=true }}, + { "board_id", types.integer }, + { "last_active", types.integer }, + { "sticky", types.boolean { default=false }}, + { "lock", types.boolean { default=false }}, + { "archive", types.boolean { default=false }}, + { "size_override", types.boolean { default=false }}, + { "save", types.boolean { default=false }} + }) + + schema.create_table("posts", { + { "id", types.serial { unique=true, primary_key=true }}, + { "post_id", types.integer }, + { "thread_id", types.integer }, + { "board_id", types.integer }, + { "timestamp", types.integer }, + { "ip", types.varchar }, + { "comment", types.text { null=true }}, + { "name", types.varchar { null=true }}, + { "trip", types.varchar { null=true }}, + { "subject", types.varchar { null=true }}, + { "password", types.varchar { null=true }}, + { "file_name", types.varchar { null=true }}, + { "file_path", types.varchar { null=true }}, + { "file_md5", types.varchar { null=true }}, + { "file_size", types.integer { null=true }}, + { "file_width", types.integer { null=true }}, + { "file_height", types.integer { null=true }}, + { "file_spoiler", types.boolean { null=true }}, + { "banned", types.boolean { default=false }} + }) + + schema.create_table("announcements", { + { "id", types.serial { unique=true, primary_key=true }}, + { "board_id", types.integer { null=true }}, + { "text", types.varchar } + }) + + schema.create_table("reports", { + { "id", types.serial { unique=true, primary_key=true }}, + { "board_id", types.integer }, + { "thread_id", types.integer }, + { "post_id", types.integer }, + { "timestamp", types.integer }, + { "num_reports", types.integer } + }) + + schema.create_table("pages", { + { "id", types.serial { unique=true, primary_key=true }}, + { "url", types.varchar { unique=true }}, + { "name", types.varchar }, + { "content", types.text } + }) + end, + [120] = function() + schema.add_column("boards", "filetype_image", types.boolean { default=true }) + schema.add_column("boards", "filetype_audio", types.boolean { default=false }) + schema.add_column("posts", "file_type", types.varchar { default="image" }) + schema.add_column("posts", "file_duration", types.varchar { null=true }) + end, + [200] = function() + + schema.rename_column("boards", "posts", "total_posts") + schema.rename_column("boards", "name", "title") + schema.rename_column("boards", "short_name", "name") + schema.rename_column("pages", "url", "slug") + schema.rename_column("pages", "name", "title") + schema.rename_column("posts", "post_id", "post_number") + + db.query("ALTER TABLE posts ALTER COLUMN file_type DROP DEFAULT") + db.query("ALTER TABLE posts ALTER COLUMN file_type DROP NOT NULL") + schema.add_column("boards", "anon_only", types.boolean { default=false }) + + local Users = require "src.models.users" + local uuid = require "resty.jit-uuid" + uuid.seed() + + schema.add_column("users", "api_key", types.varchar { default="00000000-0000-0000-0000-000000000000" }) + local users = Users:get_all() + for _, user in ipairs(users) do + user.api_key = uuid() + user:update("api_key") + end + + schema.add_column("users", "role", types.integer) + db.query("UPDATE users SET role=? WHERE janitor=true", Users.role.JANITOR) + db.query("UPDATE users SET role=? WHERE mod=true", Users.role.MOD) + db.query("UPDATE users SET role=? WHERE admin=true", Users.role.ADMIN) + db.query("UPDATE users SET role=? WHERE id=1", Users.role.OWNER) + schema.drop_column("users", "janitor") + schema.drop_column("users", "mod") + schema.drop_column("users", "admin") + + schema.add_column("posts", "reports", types.integer) + db.query("UPDATE posts SET reports=reports.num_reports FROM reports WHERE posts.id=reports.post_id") + schema.drop_table("reports") + end +} diff --git a/website/backend/app/mime.types b/website/backend/app/mime.types new file mode 100755 index 0000000..5d132eb --- /dev/null +++ b/website/backend/app/mime.types @@ -0,0 +1,79 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + + application/java-archive jar war ear; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream eot; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/website/backend/app/nginx.conf b/website/backend/app/nginx.conf new file mode 100755 index 0000000..18767cb --- /dev/null +++ b/website/backend/app/nginx.conf @@ -0,0 +1,60 @@ +worker_processes ${{NUM_WORKERS}}; +error_log stderr notice; +daemon off; +pid logs/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include mime.types; + + client_max_body_size ${{BODY_SIZE}}; + client_body_buffer_size ${{BODY_SIZE}}; + + lua_package_path "${{LUA_PATH}};;"; + lua_package_cpath "${{LUA_CPATH}};;"; + + init_worker_by_lua_block { + local uuid = require "resty.jit-uuid" + uuid.seed() + } + + init_by_lua_block { + require "lfs" + require "lpeg" + require "ltn12" + require "markdown" + require "mime" + require "socket" + } + + resolver 127.0.0.11; + + server { + listen ${{PORT}}; + lua_code_cache ${{CODE_CACHE}}; + + location / { + default_type text/html; + content_by_lua 'require("lapis").serve("app")'; + } + + location /static/ { + alias static/; + } + + location /files/ { + alias ../data/files/; + } + + location /favicon.ico { + alias ../data/favicon.ico; + } + + location /robots.txt { + alias ../data/robots.txt; + } + } +} diff --git a/website/backend/app/spec/app_spec.lua b/website/backend/app/spec/app_spec.lua new file mode 100755 index 0000000..fdcf2c5 --- /dev/null +++ b/website/backend/app/spec/app_spec.lua @@ -0,0 +1,17 @@ + +local mock_request = require("lapis.spec.request").mock_request +local app = require("app") + +describe("lapischan", function() + require("lapis.spec").use_test_env() + + setup(function() + require("lapis.db.migrations").run_migrations(require("migrations")) + end) + + it("loads install page", function() + local status, body = mock_request(app, "/") + assert.same(200, status) + assert.truthy(body:find("Install Lapis-chan", 1, true)) + end) +end) diff --git a/website/backend/app/src/apps/api.lua b/website/backend/app/src/apps/api.lua new file mode 100755 index 0000000..e989e68 --- /dev/null +++ b/website/backend/app/src/apps/api.lua @@ -0,0 +1,21 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.include = function(self, a) + self.__class.include(self, a, nil, self) +end + +app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_auth" })) +-- FIXME: app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_ban" })) +app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_locale" })) + +app:include("apps.api.core") +app:include("apps.api.announcements") +app:include("apps.api.bans") +app:include("apps.api.boards") +app:include("apps.api.pages") +app:include("apps.api.users") + +return app diff --git a/website/backend/app/src/apps/api/announcements.lua b/website/backend/app/src/apps/api/announcements.lua new file mode 100755 index 0000000..369532c --- /dev/null +++ b/website/backend/app/src/apps/api/announcements.lua @@ -0,0 +1,14 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.announcements." +app.path = "/api/announcements" + +app:match("announcements", "", capture({ on_error=handle, r2(require "apps.api.announcements.announcements") })) +app:match("announcement", "/:uri_announcement[%d]", capture({ on_error=handle, r2(require "apps.api.announcements.announcement") })) +app:match("global", "/global", capture({ on_error=handle, r2(require "apps.api.announcements.global") })) + +return app diff --git a/website/backend/app/src/apps/api/announcements/announcement.lua b/website/backend/app/src/apps/api/announcements/announcement.lua new file mode 100755 index 0000000..8d2a35a --- /dev/null +++ b/website/backend/app/src/apps/api/announcements/announcement.lua @@ -0,0 +1,64 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Announcements = models.announcements + +function action:GET() + + -- Get Announcement + local announcement = assert_error(Announcements:get(self.params.uri_announcement)) + Announcements:format_from_db(announcement) + + return { + status = ngx.HTTP_OK, + json = announcement + } +end + +function action:PUT() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + id = self.params.uri_announcement, + board_id = tonumber(self.params.board_id), + text = self.params.text, + } + trim_filter(params) + Announcements:format_to_db(params) + assert_valid(params, Announcements.valid_record) + + -- Modify Announcement + local announcement = assert_error(Announcements:modify(params)) + Announcements:format_from_db(announcement) + + return { + status = ngx.HTTP_OK, + json = announcement + } +end + +function action:DELETE() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Delete Announcement + local announcement = assert_error(Announcements:delete(self.params.uri_announcement)) + + return { + status = ngx.HTTP_OK, + json = { + id = announcement.id, + text = announcement.text + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/announcements/announcements.lua b/website/backend/app/src/apps/api/announcements/announcements.lua new file mode 100755 index 0000000..1b6c686 --- /dev/null +++ b/website/backend/app/src/apps/api/announcements/announcements.lua @@ -0,0 +1,51 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Announcements = models.announcements + +function action:GET() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Get all Announcements + local announcements = assert_error(Announcements:get_all()) + for _, announcement in ipairs(announcements) do + Announcements:format_from_db(announcement) + end + + return { + status = ngx.HTTP_OK, + json = announcements + } +end + +function action:POST() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + board_id = tonumber(self.params.board_id), + text = self.params.text, + } + trim_filter(params) + Announcements:format_to_db(params) + assert_valid(params, Announcements.valid_record) + + -- Create Announcement + local announcement = assert_error(Announcements:new(params)) + Announcements:format_from_db(announcement) + + return { + status = ngx.HTTP_OK, + json = announcement + } +end + +return action diff --git a/website/backend/app/src/apps/api/announcements/global.lua b/website/backend/app/src/apps/api/announcements/global.lua new file mode 100755 index 0000000..dde5dc6 --- /dev/null +++ b/website/backend/app/src/apps/api/announcements/global.lua @@ -0,0 +1,21 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Announcements = models.announcements + +function action.GET() + + -- Get global Announcements + local announcements = assert_error(Announcements:get_global()) + for _, announcement in ipairs(announcements) do + Announcements:format_from_db(announcement) + end + + return { + status = ngx.HTTP_OK, + json = announcements + } +end + +return action diff --git a/website/backend/app/src/apps/api/bans.lua b/website/backend/app/src/apps/api/bans.lua new file mode 100755 index 0000000..082adc8 --- /dev/null +++ b/website/backend/app/src/apps/api/bans.lua @@ -0,0 +1,14 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.bans." +app.path = "/api/bans" + +app:match("bans", "", capture({ on_error=handle, r2(require "apps.api.bans.bans") })) +app:match("ban", "/:uri_ban[%d]", capture({ on_error=handle, r2(require "apps.api.bans.ban") })) +app:match("bans_ip", "/ip/:uri_ip", capture({ on_error=handle, r2(require "apps.api.bans.bans_ip") })) + +return app diff --git a/website/backend/app/src/apps/api/bans/ban.lua b/website/backend/app/src/apps/api/bans/ban.lua new file mode 100755 index 0000000..fadc512 --- /dev/null +++ b/website/backend/app/src/apps/api/bans/ban.lua @@ -0,0 +1,70 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Bans = models.bans + +function action:GET() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Get Ban + local ban = assert_error(Bans:get(self.params.uri_ban)) + Bans:format_from_db(ban) + + return { + status = ngx.HTTP_OK, + json = ban + } +end + +function action:PUT() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Validate parameters + local params = { + id = self.params.uri_ban, + board_id = tonumber(self.params.board_id), + ip = self.params.ip, + reason = self.params.reason, + time = os.time(), + duration = tonumber(self.params.duration) + } + trim_filter(params) + Bans:format_to_db(params) + assert_valid(params, Bans.valid_record) + + -- Modify Ban + local ban = assert_error(Bans:modify(params)) + Bans:format_from_db(ban) + + return { + status = ngx.HTTP_OK, + json = ban + } +end + +function action:DELETE() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Delete Ban + local ban = assert_error(Bans:delete(self.params.uri_ban)) + + return { + status = ngx.HTTP_OK, + json = { + id = ban.id, + ip = ban.ip, + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/bans/bans.lua b/website/backend/app/src/apps/api/bans/bans.lua new file mode 100755 index 0000000..e2bfe82 --- /dev/null +++ b/website/backend/app/src/apps/api/bans/bans.lua @@ -0,0 +1,54 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Bans = models.bans + +function action:GET() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Get all Bans + local bans = assert_error(Bans:get_all()) + for _, ban in ipairs(bans) do + Bans:format_from_db(ban) + end + + return { + status = ngx.HTTP_OK, + json = bans + } +end + +function action:POST() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Validate parameters + local params = { + board_id = tonumber(self.params.board_id), + ip = self.params.ip, + reason = self.params.reason, + time = os.time(), + duration = tonumber(self.params.duration) + } + trim_filter(params) + Bans:format_to_db(params) + assert_valid(params, Bans.valid_record) + + -- Create Ban + local ban = assert_error(Bans:new(params)) + Bans:format_from_db(ban) + + return { + status = ngx.HTTP_OK, + json = ban + } +end + +return action diff --git a/website/backend/app/src/apps/api/bans/bans_ip.lua b/website/backend/app/src/apps/api/bans/bans_ip.lua new file mode 100755 index 0000000..f4ce35f --- /dev/null +++ b/website/backend/app/src/apps/api/bans/bans_ip.lua @@ -0,0 +1,25 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local role = require "utils.role" +local models = require "models" +local Bans = models.bans + +function action:GET() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Get Bans + local bans = assert_error(Bans:get_ip(self.params.uri_ip)) + for _, ban in ipairs(bans) do + Bans:format_from_db(ban) + end + + return { + status = ngx.HTTP_OK, + json = bans + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards.lua b/website/backend/app/src/apps/api/boards.lua new file mode 100755 index 0000000..3e493f0 --- /dev/null +++ b/website/backend/app/src/apps/api/boards.lua @@ -0,0 +1,23 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.boards." +app.path = "/api/boards" + +app:match("boards", "", capture({ on_error=handle, r2(require "apps.api.boards.boards") })) +app:match("board", "/:uri_board", capture({ on_error=handle, r2(require "apps.api.boards.board") })) +app:match("announcements", "/:uri_board/announcements", capture({ on_error=handle, r2(require "apps.api.boards.announcements") })) +app:match("bans", "/:uri_board/bans", capture({ on_error=handle, r2(require "apps.api.boards.bans") })) +app:match("reports", "/:uri_board/reports", capture({ on_error=handle, r2(require "apps.api.boards.reports") })) +app:match("threads", "/:uri_board/threads(/pages/:uri_page[%d])", capture({ on_error=handle, r2(require "apps.api.boards.threads") })) +app:match("archived", "/:uri_board/threads/archived", capture({ on_error=handle, r2(require "apps.api.boards.archived") })) +app:match("thread", "/:uri_board/threads/:uri_thread[%d]", capture({ on_error=handle, r2(require "apps.api.boards.thread") })) +app:match("thread.reports", "/:uri_board/threads/:uri_thread[%d]/reports", capture({ on_error=handle, r2(require "apps.api.boards.thread_reports") })) +app:match("posts", "/:uri_board(/threads/:uri_thread[%d])/posts", capture({ on_error=handle, r2(require "apps.api.boards.posts") })) +app:match("post", "/:uri_board/threads/:uri_thread[%d]/posts/:uri_post[%d]", capture({ on_error=handle, r2(require "apps.api.boards.post") })) +app:match("post.reports", "/:uri_board/threads/:uri_thread[%d]/posts/:uri_post[%d]/reports", capture({ on_error=handle, r2(require "apps.api.boards.post_reports") })) + +return app diff --git a/website/backend/app/src/apps/api/boards/announcements.lua b/website/backend/app/src/apps/api/boards/announcements.lua new file mode 100755 index 0000000..ebeb3a3 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/announcements.lua @@ -0,0 +1,25 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Announcements = models.announcements +local Boards = models.boards + +function action:GET() + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_board)) + + -- Get Announcements + local announcements = Announcements:get_board(board.id) + for _, announcement in ipairs(announcements) do + Announcements:format_from_db(announcement) + end + + return { + status = ngx.HTTP_OK, + json = announcements + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/archived.lua b/website/backend/app/src/apps/api/boards/archived.lua new file mode 100755 index 0000000..965984e --- /dev/null +++ b/website/backend/app/src/apps/api/boards/archived.lua @@ -0,0 +1,24 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Boards = models.boards +local Threads = models.threads + +function action:GET() + + local board = assert_error(Boards:get(self.params.uri_name)) + + -- Get Threads + local threads = board:get_archived() + for _, thread in ipairs(threads) do + --Threads:format_from_db(thread) + end + + return { + status = ngx.HTTP_OK, + json = threads + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/bans.lua b/website/backend/app/src/apps/api/boards/bans.lua new file mode 100755 index 0000000..cd2dc92 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/bans.lua @@ -0,0 +1,29 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local role = require "utils.role" +local models = require "models" +local Bans = models.bans +local Boards = models.boards + +function action:GET() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_board)) + + -- Get Bans + local bans = Bans:get_board(board.id) + for _, ban in ipairs(bans) do + Bans:format_from_db(ban) + end + + return { + status = ngx.HTTP_OK, + json = bans + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/board.lua b/website/backend/app/src/apps/api/boards/board.lua new file mode 100755 index 0000000..8d43c49 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/board.lua @@ -0,0 +1,114 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Boards = models.boards + +function action:GET() + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_board)) + Boards:format_from_db(board) + + return { + status = ngx.HTTP_OK, + json = board + } +end + +function action:PUT() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + name = self.params.name, + title = self.params.title, + subtext = self.params.subtext, + rules = self.params.rules, + ban_message = self.params.ban_message, + anon_name = self.params.anon_name, + theme = self.params.theme, + total_posts = tonumber(self.params.total_posts), + pages = tonumber(self.params.pages), + threads_per_page = tonumber(self.params.threads_per_page), + anon_only = self.params.anon_only, + text_only = self.params.text_only, + draw = self.params.draw, + thread_file = self.params.thread_file, + thread_comment = self.params.thread_comment, + thread_file_limit = self.params.thread_file_limit, + post_file = self.params.post_file, + post_comment = self.params.post_comment, + post_limit = tonumber(self.params.post_limit), + archive = self.params.archive, + archive_time = tonumber(self.params.archive_time), + group = self.params.group, + filetype_image = self.params.filetype_image, + filetype_audio = self.params.filetype_audio + } + trim_filter(params) + Boards:format_to_db(params) + assert_valid(params, Boards.valid_record) + + -- Check if board being modified is reusing name and title + local reuse_name = false + local reuse_title = false + + if params.name or params.title then + local board = Boards:get(self.params.uri_board) + reuse_name = board.name == params.name + reuse_title = board.title == params.title + end + + -- Verify unique or current name and title + if not (reuse_name and reuse_title) then + local boards = Boards:get_all() + for _, board in ipairs(boards) do + if not reuse_name and board.name == params.name then + yield_error("FIXME") + end + + if not reuse_title and board.title == params.title then + yield_error("FIXME") + end + end + end + + -- Modify board + local board = assert_error(Boards:modify(params, self.params.uri_board)) + Boards:format_from_db(board) + + return { + status = ngx.HTTP_OK, + json = board + } +end + +function action:DELETE() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Delete board + local board = assert_error(Boards:delete(self.params.uri_board)) + + -- TODO: delete adjacent data (announcements, bans, threads, posts, files) + -- probably best done by adding actual FK constraints with "ON DELETE CASCADE" + + return { + status = ngx.HTTP_OK, + json = { + id = board.id, + name = board.name, + title = board.title + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/boards.lua b/website/backend/app/src/apps/api/boards/boards.lua new file mode 100755 index 0000000..146de1d --- /dev/null +++ b/website/backend/app/src/apps/api/boards/boards.lua @@ -0,0 +1,79 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Boards = models.boards + +function action.GET() + + -- Get Boards + local boards = assert_error(Boards:get_all()) + for _, board in ipairs(boards) do + Boards:format_from_db(board) + end + + return { + status = ngx.HTTP_OK, + json = boards + } +end + +function action:POST() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + name = self.params.name, + title = self.params.title, + subtext = self.params.subtext, + rules = self.params.rules, + ban_message = self.params.ban_message, + anon_name = self.params.anon_name, + theme = self.params.theme, + total_posts = tonumber(self.params.total_posts), + pages = tonumber(self.params.pages), + threads_per_page = tonumber(self.params.threads_per_page), + anon_only = self.params.anon_only, + text_only = self.params.text_only, + draw = self.params.draw, + thread_file = self.params.thread_file, + thread_comment = self.params.thread_comment, + thread_file_limit = self.params.thread_file_limit, + post_file = self.params.post_file, + post_comment = self.params.post_comment, + post_limit = tonumber(self.params.post_limit), + archive = self.params.archive, + archive_time = tonumber(self.params.archive_time), + group = self.params.group, + filetype_image = self.params.filetype_image, + filetype_audio = self.params.filetype_audio + } + trim_filter(params) + Boards:format_to_db(params) + assert_valid(params, Boards.valid_record) + + -- Verify unique name and title + local boards = Boards:get_all() + for _, board in ipairs(boards) do + if board.name == params.name or board.title == params.title then + yield_error("FIXME") + end + end + + -- Create board + local board = assert_error(Boards:new(params)) + Boards:format_from_db(board) + + return { + status = ngx.HTTP_OK, + json = board + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/post.lua b/website/backend/app/src/apps/api/boards/post.lua new file mode 100755 index 0000000..bc4967c --- /dev/null +++ b/website/backend/app/src/apps/api/boards/post.lua @@ -0,0 +1,92 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local models = require "models" +local Boards = models.boards +local Threads = models.threads +local Posts = models.posts + +function action:GET() + + local board = assert_error(Boards:get(self.params.uri_name)) + + -- Get Post + local post = assert_error(Posts:get(board.id, self.params.uri_id)) + --Posts:format_from_db(post) + + return { + status = ngx.HTTP_OK, + json = post + } +end + +function action:PUT() + + -- Validate parameters + local params = { + comment = self.params.comment, + subject = self.params.subject, + file_spoiler = self.params.file_spoiler + } + Posts:format_to_db(params) + trim_filter(params) + assert_valid(params, Posts.valid_record) + + -- Modify post + local post = assert_error(Posts:modify(params)) + Posts:format_from_db(post) + + return { + status = ngx.HTTP_OK, + json = post + } +end + +function action:DELETE() + + --[[ FIXME: needs proper auth! + -- MODS = FAGS + if type(session) == "table" and + (session.admin or session.mod or session.janitor) then + rm_post(board.name) + success = true + -- Override password + elseif type(session) == "string" and + session == "override" then + rm_post(board.name) + success = true + -- Password has to match! + elseif post and session.password and + post.password == session.password then + rm_post(board.name) + success = true + end + --]] + + -- Delete post + local post = assert_error(Posts:get_post_by_id(self.params.uri_id)) + local thread = post:get_thread() + local op = thread:get_op() + + if post.id == op.id then + assert_error(Threads:delete(thread.id)) + + local posts = thread:get_posts() + for _, p in ipairs(posts) do + assert_error(Posts:delete(p.id)) + end + else + assert_error(Posts:delete(post.id)) + end + + return { + status = ngx.HTTP_OK, + json = { + id = post.id + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/post_reports.lua b/website/backend/app/src/apps/api/boards/post_reports.lua new file mode 100755 index 0000000..8213a3f --- /dev/null +++ b/website/backend/app/src/apps/api/boards/post_reports.lua @@ -0,0 +1,21 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Boards = models.boards + +function action:GET() + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_name)) + + -- Get Reports + local reports = board:get_reports() + + return { + status = ngx.HTTP_OK, + json = reports + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/posts.lua b/website/backend/app/src/apps/api/boards/posts.lua new file mode 100755 index 0000000..6e7e375 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/posts.lua @@ -0,0 +1,105 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local models = require "models" +local Boards = models.boards +local Threads = models.threads +local Posts = models.posts + +function action:GET() + + local posts + + if self.params.uri_thread then + local thread = assert_error(Threads:get(self.params.uri_thread)) + posts = thread:get_posts() + else + local board = assert_error(Boards:get(self.params.uri_board)) + posts = board:get_posts() + end + + return { + status = ngx.HTTP_OK, + json = posts + } +end + +function action:POST() + local now = os.time() + local board = assert_error(Boards:get(self.params.uri_name)) + local thread = Threads:get(self.params.uri_id) + local op = thread and false or true + + -- Create a new thread if no thread exists + if not thread then + -- Validate parameters + local params = { + board_id = board.id, + last_active = now, + sticky = self.params.sticky, + lock = self.params.lock, + size_override = self.params.size_override, + save = self.params.save + } + + -- Only admins and mods can flag threads + -- FIXME: API has no session, need proper auth! + if not self.session.admin or self.session.mod then + params.sticky = nil + params.lock = nil + params.size_override = nil + params.save = nil + end + + --Threads:format_to_db(params) + trim_filter(params) + assert_valid(params, Threads.valid_record) + + -- Create thread + thread = assert_error(Threads:new(params)) + --Threads:format_from_db(thread) + end + + -- FIXME: there needs to be a better way to do this to avoid race conditions... + board.total_posts = board.total_posts + 1 + + -- Validate parameters + local params = { + post_id = board.total_posts, + thread_id = thread.id, + board_id = board.id, + timestamp = now, + ip = self.params.ip, + comment = self.params.comment, + name = self.params.name, + trip = self.params.trip, + subject = self.params.subject, + password = self.params.password, + file_name = self.params.file_name, + file_path = self.params.file_path, + file_type = self.params.file_type, + file_md5 = self.params.file_md5, + file_size = self.params.file_size, + file_width = self.params.file_width, + file_height = self.params.file_height, + file_duration = self.params.file_duration, + file_spoiler = self.params.file_spoiler, + file_content = self.params.file_content -- FIXME: we probably want to base64 decode this in format_to_db + } + Posts:format_to_db(params) + trim_filter(params) + assert_valid(params, Posts.valid_record) + + -- Create post + local post = assert_error(Posts:new(params, board, op)) + Posts:format_from_db(post) + + return { + status = ngx.HTTP_OK, + json = post + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/reports.lua b/website/backend/app/src/apps/api/boards/reports.lua new file mode 100755 index 0000000..0ccfc08 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/reports.lua @@ -0,0 +1,29 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local role = require "utils.role" +local models = require "models" +local Boards = models.boards +local Posts = models.posts + +function action:GET() + + -- Verify the User's permissions + assert_error(role.mod(self.api_user)) + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_board)) + + -- Get Reported posts + local posts = Posts:get_board_reports(board.id) + for _, post in ipairs(posts) do + Posts:format_from_db(post) + end + + return { + status = ngx.HTTP_OK, + json = posts + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/thread.lua b/website/backend/app/src/apps/api/boards/thread.lua new file mode 100755 index 0000000..ddbedc8 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/thread.lua @@ -0,0 +1,69 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Threads = models.threads + +function action:GET() + + -- Get Thread + local thread = assert_error(Threads:get(self.params.uri_id)) + --Threads:format_from_db(thread) + + return { + status = ngx.HTTP_OK, + json = thread + } +end + +function action:PUT() + + local params = { id=self.params.uri_id } + + -- Extract flag from URI + if self.params.uri_value:lower() == "true" or + self.params.uri_value:lower() == "t" or + tonumber(self.params.uri_value) == 1 then + self.params.uri_value = true + elseif self.params.uri_value:lower() == "false" or + self.params.uri_value:lower() == "f" or + tonumber(self.params.uri_value) == 0 then + self.params.uri_value = false + else + return { + status = ngx.HTTP_BAD_REQUEST, + json = {} + } + end + + -- Extract variable from URI + if self.params.uri_action == "sticky" then + params.sticky = self.params.uri_value + elseif self.params.uri_action == "lock" then + params.lock = self.params.uri_value + elseif self.params.uri_action == "save" then + params.save = self.params.uri_value + elseif self.params.uri_action == "size_override" then + params.size_override = self.params.uri_value + else + return { + status = ngx.HTTP_BAD_REQUEST, + json = {} + } + end + + -- Modify thread + local thread = assert_error(Threads:modify(params)) + --Threads:format_from_db(thread) + + return { + status = ngx.HTTP_OK, + json = thread + } +end + +function action:DELETE() + +end + +return action diff --git a/website/backend/app/src/apps/api/boards/thread_reports.lua b/website/backend/app/src/apps/api/boards/thread_reports.lua new file mode 100755 index 0000000..8213a3f --- /dev/null +++ b/website/backend/app/src/apps/api/boards/thread_reports.lua @@ -0,0 +1,21 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Boards = models.boards + +function action:GET() + + -- Get Board + local board = assert_error(Boards:get(self.params.uri_name)) + + -- Get Reports + local reports = board:get_reports() + + return { + status = ngx.HTTP_OK, + json = reports + } +end + +return action diff --git a/website/backend/app/src/apps/api/boards/threads.lua b/website/backend/app/src/apps/api/boards/threads.lua new file mode 100755 index 0000000..6ed4530 --- /dev/null +++ b/website/backend/app/src/apps/api/boards/threads.lua @@ -0,0 +1,35 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local models = require "models" +local Boards = models.boards +local Threads = models.threads + +function action:GET() + + local threads, pages + local board = assert_error(Boards:get(self.params.uri_name)) + + -- Get Threads + if self.params.uri_page then + local paginator = board:get_threads_paginated({ per_page=board.threads_per_page }) + threads = paginator:get_page(self.params.uri_page) + pages = paginator:num_pages() + else + threads = board:get_threads() + end + + for _, thread in ipairs(threads) do + --Threads:format_from_db(thread) + end + + return { + status = ngx.HTTP_OK, + json = { + threads = threads, + pages = pages + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/core.lua b/website/backend/app/src/apps/api/core.lua new file mode 100755 index 0000000..f005582 --- /dev/null +++ b/website/backend/app/src/apps/api/core.lua @@ -0,0 +1,13 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.core." +app.path = "/api" + +app:match("root", "", capture({ on_error=handle, r2(require "apps.api.core.root") })) +app:match("login", "/login", capture({ on_error=handle, r2(require "apps.api.core.login") })) + +return app diff --git a/website/backend/app/src/apps/api/core/login.lua b/website/backend/app/src/apps/api/core/login.lua new file mode 100755 index 0000000..f39238d --- /dev/null +++ b/website/backend/app/src/apps/api/core/login.lua @@ -0,0 +1,35 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local models = require "models" +local Users = models.users + +function action:POST() + + -- Normally we'd process these inputs a bit but in the case of + -- authentication credentials, we want to use the raw user inputs. + local params = { + username = self.params.username, + password = self.params.password + } + + -- Early exit if credentials not sent + if not params.username or not params.password then + yield_error("FIXME") + end + + local user = assert_error(Users:login(params)) + + return { + status = ngx.HTTP_OK, + json = { + id = user.id, + username = user.username, + role = user.role, + api_key = user.api_key + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/core/root.lua b/website/backend/app/src/apps/api/core/root.lua new file mode 100755 index 0000000..aabefc0 --- /dev/null +++ b/website/backend/app/src/apps/api/core/root.lua @@ -0,0 +1,11 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") + +function action.GET() + return { + status = ngx.HTTP_OK, + json = {} + } +end + +return action diff --git a/website/backend/app/src/apps/api/internal/action_base.lua b/website/backend/app/src/apps/api/internal/action_base.lua new file mode 100755 index 0000000..06adb3c --- /dev/null +++ b/website/backend/app/src/apps/api/internal/action_base.lua @@ -0,0 +1,17 @@ +local ngx = _G.ngx +local action = {} + +local function errors() + return { + status = ngx.HTTP_NOT_ALLOWED, + json = {} + } +end + +action.__index = action +action.GET = errors +action.POST = errors +action.PUT = errors +action.DELETE = errors + +return action diff --git a/website/backend/app/src/apps/api/internal/before_auth.lua b/website/backend/app/src/apps/api/internal/before_auth.lua new file mode 100755 index 0000000..0aaee28 --- /dev/null +++ b/website/backend/app/src/apps/api/internal/before_auth.lua @@ -0,0 +1,41 @@ +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local mime = require "mime" +local models = require "models" +local Users = models.users + +return function(self) + + if self.req.headers["Authorization"] then + + -- Decode auth info + local auth = mime.unb64(self.req.headers["Authorization"]:sub(7)) + local username, api_key = auth:match("^(.+)%:(.+)$") + + -- DENY if Authorization is malformed + if not username or not api_key then + yield_error("FIXME: Corrupt auth!") + end + + -- DENY if a user's key isn't properly set + if api_key == Users.default_key then + yield_error("FIXME: Bad auth!") + end + + local params = { + username = username, + api_key = api_key + } + + -- Get User + self.api_user = assert_error(Users:get_api(params)) + Users:format_from_db(self.api_user) + return + end + + -- Set basic User + self.api_user = { + id = -1, + role = -1 + } +end diff --git a/website/backend/app/src/apps/api/internal/before_locale.lua b/website/backend/app/src/apps/api/internal/before_locale.lua new file mode 100755 index 0000000..8a14fb3 --- /dev/null +++ b/website/backend/app/src/apps/api/internal/before_locale.lua @@ -0,0 +1,20 @@ +local i18n = require "i18n" +local lfs = require "lfs" + +return function(self) + + -- Set locale + self.i18n = i18n + local locale = self.req.headers["Content-Language"] or "en" + i18n.setLocale(locale) + i18n.loadFile("src/locale/en.lua") + + -- Get locale file + local path = "src/locale" + for file in lfs.dir(path) do + local name, ext = string.match(file, "^(.+)%.(.+)$") + if name == locale and ext == "lua" then + i18n.loadFile(string.format("%s/%s.lua", path, name)) + end + end +end diff --git a/website/backend/app/src/apps/api/pages.lua b/website/backend/app/src/apps/api/pages.lua new file mode 100755 index 0000000..86a02f4 --- /dev/null +++ b/website/backend/app/src/apps/api/pages.lua @@ -0,0 +1,13 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.pages." +app.path = "/api/pages" + +app:match("pages", "", capture({ on_error=handle, r2(require "apps.api.pages.pages") })) +app:match("page", "/:uri_page", capture({ on_error=handle, r2(require "apps.api.pages.page") })) + +return app diff --git a/website/backend/app/src/apps/api/pages/page.lua b/website/backend/app/src/apps/api/pages/page.lua new file mode 100755 index 0000000..694f7bc --- /dev/null +++ b/website/backend/app/src/apps/api/pages/page.lua @@ -0,0 +1,60 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Pages = models.pages + +function action:GET() + local page = assert_error(Pages:get(self.params.uri_page)) + + return { + status = ngx.HTTP_OK, + json = page + } +end + +function action:PUT() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + slug = self.params.slug, + title = self.params.title, + content = self.params.content + } + trim_filter(params) + Pages:format_to_db(params) + assert_valid(params, Pages.valid_record) + + -- Modify page + local page = assert_error(Pages:modify(params, self.params.uri_page)) + + return { + status = ngx.HTTP_OK, + json = page + } +end + +function action:DELETE() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Delete Page + local page = assert_error(Pages:delete(self.params.uri_page)) + + return { + status = ngx.HTTP_OK, + json = { + slug = page.slug, + title = page.title + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/pages/pages.lua b/website/backend/app/src/apps/api/pages/pages.lua new file mode 100755 index 0000000..92d968b --- /dev/null +++ b/website/backend/app/src/apps/api/pages/pages.lua @@ -0,0 +1,45 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Pages = models.pages + +function action.GET() + + -- Get all Pages + local pages = assert_error(Pages:get_all()) + + return { + status = ngx.HTTP_OK, + json = pages + } +end + +function action:POST() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + slug = self.params.slug, + title = self.params.title, + content = self.params.content, + } + trim_filter(params) + Pages:format_to_db(params) + assert_valid(params, Pages.valid_record) + + -- Create Page + local page = assert_error(Pages:new(params)) + + return { + status = ngx.HTTP_OK, + json = page + } +end + +return action diff --git a/website/backend/app/src/apps/api/users.lua b/website/backend/app/src/apps/api/users.lua new file mode 100755 index 0000000..cf5c86d --- /dev/null +++ b/website/backend/app/src/apps/api/users.lua @@ -0,0 +1,13 @@ +local lapis = require "lapis" +local capture = require("lapis.application").capture_errors_json +local r2 = require("lapis.application").respond_to +local handle = require("utils.error").handle +local app = lapis.Application() +app.__base = app +app.name = "api.users." +app.path = "/api/users" + +app:match("users", "", capture({ on_error=handle, r2(require "apps.api.users.users") })) +app:match("user", "/:uri_user", capture({ on_error=handle, r2(require "apps.api.users.user") })) + +return app diff --git a/website/backend/app/src/apps/api/users/user.lua b/website/backend/app/src/apps/api/users/user.lua new file mode 100755 index 0000000..fd31fba --- /dev/null +++ b/website/backend/app/src/apps/api/users/user.lua @@ -0,0 +1,108 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Users = models.users + +function action:GET() + + local user = assert_error(Users:get(self.params.uri_user)) + Users:format_from_db(user) + + -- Verify the User's permissions + local is_admin = role.admin(self.api_user) + local is_user = self.api_user.id == user.id + if not is_admin and not is_user then + yield_error("FIXME") + end + + return { + status = ngx.HTTP_OK, + json = user + } +end + +function action:PUT() + + local user = assert_error(Users:get(self.params.uri_user)) + + -- Verify the User's permissions + local is_admin = role.admin(self.api_user) + local is_user = self.api_user.id == user.id + local is_auth = self.api_user.role > user.role + if (not is_admin and not is_user) or not is_auth then + yield_error("FIXME") + end + + -- Validate parameters + local params = { + username = self.params.username, + password = self.params.password, + confirm = self.params.confirm, + role = tonumber(self.params.role), + api_key = self.params.api_key + } + trim_filter(params) + Users:format_to_db(params) + assert_valid(params, Users.valid_record) + + -- If no role was sent, don't update it + -- This is kind of dumb since we're just setting it from nil to -1 and back + -- to nil, but I want to keep the format_to_db in case of future formatting + -- concerns. + if params.role == Users.role.INVALID then + params.role = nil + end + + if params.role then + + -- Only admins can change a role + if not is_admin then + yield_error("FIXME") + end + + -- Cannot elevate to or above own role + if self.api_user.role <= params.role then + yield_error("FIXME") + end + end + + -- Modify User + user = assert_error(Users:modify(params, self.params.uri_user, self.params.password)) + Users:format_from_db(user) + + return { + status = ngx.HTTP_OK, + json = user + } +end + +function action:DELETE() + + local user = assert_error(Users:get(self.params.uri_user)) + + -- Verify the User's permissions + local is_admin = role.admin(self.api_user) + local is_user = self.api_user.id == user.id + local is_auth = self.api_user.role > user.role + if not is_admin and not is_user and not is_auth then + yield_error("FIXME") + end + + -- Delete User + user = assert_error(Users:delete(self.params.uri_user)) + + return { + status = ngx.HTTP_OK, + json = { + id = user.id, + username = user.username + } + } +end + +return action diff --git a/website/backend/app/src/apps/api/users/users.lua b/website/backend/app/src/apps/api/users/users.lua new file mode 100755 index 0000000..fbb663e --- /dev/null +++ b/website/backend/app/src/apps/api/users/users.lua @@ -0,0 +1,64 @@ +local ngx = _G.ngx +local action = setmetatable({}, require "apps.api.internal.action_base") +local assert_error = require("lapis.application").assert_error +local yield_error = require("lapis.application").yield_error +local assert_valid = require("lapis.validate").assert_valid +local trim_filter = require("lapis.util").trim_filter +local role = require "utils.role" +local models = require "models" +local Users = models.users + +function action:GET() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Get all Users + local users = assert_error(Users:get_all()) + for _, user in ipairs(users) do + Users:format_from_db(user) + end + + return { + status = ngx.HTTP_OK, + json = users + } +end + +function action:POST() + + -- Verify the User's permissions + assert_error(role.admin(self.api_user)) + + -- Validate parameters + local params = { + username = self.params.username, + password = self.params.password, + confirm = self.params.confirm, + role = tonumber(self.params.role) + } + trim_filter(params) + Users:format_to_db(params) + assert_valid(params, Users.valid_record) + + -- DENY if no role was sent + if params.role == Users.role.INVALID then + yield_error("FIXME") + end + + -- Cannot elevate to or above own role + if self.api_user.role <= params.role then + yield_error("FIXME") + end + + -- Create user + local user = assert_error(Users:new(params, self.params.password)) + Users:format_from_db(user) + + return { + status = ngx.HTTP_OK, + json = user + } +end + +return action diff --git a/website/backend/app/src/apps/web.lua b/website/backend/app/src/apps/web.lua new file mode 100755 index 0000000..6acb1ef --- /dev/null +++ b/website/backend/app/src/apps/web.lua @@ -0,0 +1,16 @@ +local lapis = require "lapis" +local app = lapis.Application() +app.__base = app +app.include = function(self, a) + self.__class.include(self, a, nil, self) +end + +app:before_filter(require "apps.web.internal.config_site") +app:before_filter(require "apps.web.internal.check_auth") +app:before_filter(require "apps.web.internal.check_ban") + +app:include("apps.web.admin") +app:include("apps.web.pages") +app:include("apps.web.boards") + +return app diff --git a/website/backend/app/src/apps/web/admin.lua b/website/backend/app/src/apps/web/admin.lua new file mode 100755 index 0000000..f37e73b --- /dev/null +++ b/website/backend/app/src/apps/web/admin.lua @@ -0,0 +1,16 @@ +local lapis = require "lapis" +local r2 = require("lapis.application").respond_to +local app = lapis.Application() +app.__base = app +app.name = "web.admin." +app.path = "/admin" +app.handle_404 = require "apps.web.internal.code_404" + +app:match("index", "", r2(require "apps.web.admin.index")) +app:match("users", "/:action/user(/:user)", r2(require "apps.web.admin.user")) +app:match("boards", "/:action/board(/:uri_name)", r2(require "apps.web.admin.board")) +app:match("announcements", "/:action/announcement(/:ann)", r2(require "apps.web.admin.announcement")) +app:match("pages", "/:action/page(/:page)", r2(require "apps.web.admin.page")) +app:match("reports", "/:action/report(/:report)", r2(require "apps.web.admin.report")) + +return app diff --git a/website/backend/app/src/apps/web/admin/announcement.lua b/website/backend/app/src/apps/web/admin/announcement.lua new file mode 100755 index 0000000..7c3bb25 --- /dev/null +++ b/website/backend/app/src/apps/web/admin/announcement.lua @@ -0,0 +1,128 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local csrf = require "lapis.csrf" +local capture = require "utils.capture" +local generate = require "utils.generate" + +return { + before = function(self) + -- Get announcements + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcements"))) + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Verify Authorization + if self.session.name then + if not self.session.admin then + assert_error(false, "err_not_admin") + end + else + return + end + + -- Display creation form + if self.params.action == "create" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("create_ann") + ) + self.announcement = self.params + return + end + + -- Display modification form + if self.params.action == "modify" then + self.announcement = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id }))) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("modify_ann") + ) + return + end + + -- Delete announcement + if self.params.action == "delete" then + self.announcement = assert_error(capture.delete(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id }))) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("deleted_ann", { self.announcement.text }) + return + end + end, + + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.announcement" } + elseif self.params.action == "modify" then + return { render = "admin.announcement" } + elseif self.params.action == "delete" then + return { render = "admin.admin" } + end + end, + + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.announcement" } + elseif self.params.action == "modify" then + return { render = "admin.announcement" } + elseif self.params.action == "delete" then + return { render = "admin.success" } + end + end, + + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Validate user input + assert_valid(self.params, { + { "text", max_length=255, exists=true } + }) + + -- Create announcement + if self.params.create_announcement then + self.announcement = assert_error(capture.post(self:url_for("api.announcements.announcements"), self.params)) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("created_ann", { self.announcement.text }) + + return { render = "admin.success" } + end + + -- Modify announcement + if self.params.modify_announcement then + self.announcement = assert_error(capture.put(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id }), self.params)) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("modified_ann", { self.announcement.text }) + + return { render = "admin.success" } + end + + return { redirect_to = self:url_for("web.admin.index") } + end +} diff --git a/website/backend/app/src/apps/web/admin/board.lua b/website/backend/app/src/apps/web/admin/board.lua new file mode 100755 index 0000000..fe05b61 --- /dev/null +++ b/website/backend/app/src/apps/web/admin/board.lua @@ -0,0 +1,136 @@ +local assert_error = require("lapis.application").assert_error +local csrf = require "lapis.csrf" +local lfs = require "lfs" +local capture = require "utils.capture" +local generate = require "utils.generate" + +return { + before = function(self) + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Verify Authorization + if not self.session.name then return end + if not self.session.admin then + assert_error(false, "err_not_admin") + end + + -- Get list of themes + self.themes = {} + for file in lfs.dir("./static/css") do + local name, ext = string.match(file, "^(.+)%.(.+)$") + if name ~= "reset" and + name ~= "posts" and + name ~= "style" and + name ~= "tegaki" and + ext == "css" then + table.insert(self.themes, name) + end + end + + -- Display creation form + if self.params.action == "create" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("create_board") + ) + self.board = self.params + + if not self.board.theme then + self.board.theme = "yotsuba_b" + end + + return + end + + -- Display modification form + if self.params.action == "modify" then + self.board = assert_error(capture.get(self:url_for("api.boards.board", { uri_name=self.params.uri_name }))) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("modify_board") + ) + return + end + + -- Delete board + if self.params.action == "delete" then + local board = assert_error(capture.delete(self:url_for("api.boards.board", { uri_name=self.params.uri_name }))) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("deleted_board", { board.name, board.title }) + return + end + end, + + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.board" } + elseif self.params.action == "modify" then + return { render = "admin.board" } + elseif self.params.action == "delete" then + return { render = "admin.admin" } + end + end, + + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.board" } + elseif self.params.action == "modify" then + return { render = "admin.board" } + elseif self.params.action == "delete" then + return { render = "admin.success" } + end + end, + + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Create new board + if self.params.create_board then + local board = assert_error(capture.post(self:url_for("api.boards.boards"), self.params)) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("created_board", { board.name, board.title }) + + return { render = "admin.success" } + end + + -- Modify board + if self.params.modify_board then + local board = assert_error(capture.put(self:url_for("api.boards.board", { uri_name=self.params.uri_name }), self.params)) + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("modified_board", { board.name, board.title }) + + return { render = "admin.success" } + end + + return { redirect_to = self:url_for("web.admin.index") } + end +} diff --git a/website/backend/app/src/apps/web/admin/index.lua b/website/backend/app/src/apps/web/admin/index.lua new file mode 100755 index 0000000..df2b31d --- /dev/null +++ b/website/backend/app/src/apps/web/admin/index.lua @@ -0,0 +1,138 @@ +local assert_error = require("lapis.application").assert_error +local csrf = require "lapis.csrf" +local capture = require "utils.capture" +local generate = require "utils.generate" +local Boards = require "models.boards" +local Pages = require "models.pages" +local Posts = require "models.posts" +local Reports = require "models.reports" +local Users = require "models.users" + +return { + before = function(self) + -- Get data + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcements"))) + self.pages = Pages:get_pages() + self.reports = Reports:get_reports() + self.users = Users:get_users() + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Verify Authorization + if self.session.name then + if not self.session.admin then + assert_error(false, "err_not_admin") + end + else + return + end + end, + + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + end + + return { render = "admin.admin" } + end, + + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + end + + return { render = "admin.admin" } + end, + + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Verify user credentials + if self.params.login then + -- Verify user + local user = assert_error(Users:verify_user(self.params)) + + -- Set username + self.session.name = user.username + + return { redirect_to = self.admin_url } + end + + -- Must be logged in as an admin! + if self.session.admin then + -- Redirect to modify user page + if self.params.modify_user then + return { redirect_to = self:url_for("web.admin.users", { action="modify", user=self.params.user }) } + end + + -- Redirect to delete user page + if self.params.delete_user then + return { redirect_to = self:url_for("web.admin.users", { action="delete", user=self.params.user }) } + end + + -- Redirect to modify board page + if self.params.modify_board then + return { redirect_to = self:url_for("web.admin.boards", { action="modify", uri_name=self.params.board }) } + end + + -- Redirect to delete board page + if self.params.delete_board then + return { redirect_to = self:url_for("web.admin.boards", { action="delete", uri_name=self.params.board }) } + end + + -- Redirect to modify announcement page + if self.params.modify_announcement then + return { redirect_to = self:url_for("web.admin.announcements", { action="modify", ann=self.params.ann }) } + end + + -- Redirect to delete announcement page + if self.params.delete_announcement then + return { redirect_to = self:url_for("web.admin.announcements", { action="delete", ann=self.params.ann }) } + end + + -- Redirect to modify page page + if self.params.modify_page then + return { redirect_to = self:url_for("web.admin.pages", { action="modify", page=self.params.page }) } + end + + -- Redirect to delete page page + if self.params.delete_page then + return { redirect_to = self:url_for("web.admin.pages", { action="delete", page=self.params.page }) } + end + + -- Redirect to reported post + if self.params.view_report then + local report = Reports:get_report_by_id(self.params.report) + local board = Boards:get_board(report.board_id) + local post = Posts:get_post(board.id, report.post_id) + local op = Posts:get_thread_op(report.thread_id) + + return { redirect_to = self:url_for("web.boards.thread", { board=board.name, thread=op.post_id, anchor="p", id=post.post_id }) } + end + + -- Redirect to delete report page + if self.params.delete_report then + return { redirect_to = self:url_for("web.admin.reports", { action="delete", report=self.params.report }) } + end + + -- Regenerate thumbnails + if self.params.regen_thumbs then + Boards:regen_thumbs() + end + + return { render = "admin.admin" } + end + + return { render = "admin.login" } + end +} diff --git a/website/backend/app/src/apps/web/admin/page.lua b/website/backend/app/src/apps/web/admin/page.lua new file mode 100755 index 0000000..fce2138 --- /dev/null +++ b/website/backend/app/src/apps/web/admin/page.lua @@ -0,0 +1,164 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local csrf = require "lapis.csrf" +local generate = require "utils.generate" +local Boards = require "models.boards" +local Pages = require "models.pages" + +return { + before = function(self) + -- Get all board data + self.boards = Boards:get_boards() + + -- Get all page data + self.pages = Pages:get_pages() + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Verify Authorization + if self.session.name then + if not self.session.admin then + assert_error(false, "err_not_admin") + end + else + return + end + + -- Display creation form + if self.params.action == "create" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("create_page") + ) + self.page = self.params + return + end + + -- Display modification form + if self.params.action == "modify" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("modify_page") + ) + self.page = Pages:get_page(self.params.page) + return + end + + -- Delete page + if self.params.action == "delete" then + local page = Pages:get_page(self.params.page) + assert_error(Pages:delete_page(page)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("deleted_page", { page.slug, page.title }) + return + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.page" } + elseif self.params.action == "modify" then + return { render = "admin.page" } + elseif self.params.action == "delete" then + return { render = "admin.admin" } + end + end, + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.page" } + elseif self.params.action == "modify" then + return { render = "admin.page" } + elseif self.params.action == "delete" then + return { render = "admin.success" } + end + end, + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Validate user input + assert_valid(self.params, { + { "slug", max_length=255, exists=true }, + { "title", max_length=255, exists=true } + }) + + -- Create new page + if self.params.create_page then + local sl = string.lower + -- Verify unique names + for _, page in ipairs(self.pages) do + if sl(page.slug) == sl(self.params.slug) then + assert_error(false, "err_slug_used") + end + end + + -- Create page + local page = assert_error(Pages:create_page(self.params)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("created_page", { page.slug, page.title }) + + return { render = "admin.success" } + end + + -- Modify page + if self.params.modify_page then + local discard = { + "page", + "modify_page", + "ip", + "action", + "csrf_token", + "old" + } + + local page = Pages:get_page(self.params.old) + + -- Fill in board with new data + for k, param in pairs(self.params) do + page[k] = param + end + + -- Get rid of form trash + for _, param in ipairs(discard) do + page[param] = nil + end + + assert_error(Pages:modify_page(page)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("modified_page", { page.slug, page.title }) + + return { render = "admin.success" } + end + + return { redirect_to = self:url_for("web.admin.index") } + end +} diff --git a/website/backend/app/src/apps/web/admin/report.lua b/website/backend/app/src/apps/web/admin/report.lua new file mode 100755 index 0000000..0ab4b87 --- /dev/null +++ b/website/backend/app/src/apps/web/admin/report.lua @@ -0,0 +1,64 @@ +local assert_error = require("lapis.application").assert_error +local csrf = require "lapis.csrf" +local generate = require "utils.generate" +local Boards = require "models.boards" +local Reports = require "models.reports" + +return { + before = function(self) + -- Get data + self.boards = Boards:get_boards() + self.reports = Reports:get_reports() + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Verify Authorization + if self.session.name then + if not self.session.admin then + assert_error(false, "err_not_admin") + end + else + return + end + + -- Delete report + if self.params.action == "delete" then + local report = Reports:get_report_by_id(self.params.report) + assert_error(Reports:delete_report(report)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("deleted_report", { report.id }) + return + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "delete" then + return { render = "admin.admin" } + end + end, + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "delete" then + return { render = "admin.success" } + end + end, + POST = function(self) + return { redirect_to = self:url_for("web.admin.index") } + end +} diff --git a/website/backend/app/src/apps/web/admin/user.lua b/website/backend/app/src/apps/web/admin/user.lua new file mode 100755 index 0000000..b6ae735 --- /dev/null +++ b/website/backend/app/src/apps/web/admin/user.lua @@ -0,0 +1,190 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local csrf = require "lapis.csrf" +local generate = require "utils.generate" +local Boards = require "models.boards" +local Users = require "models.users" + +return { + before = function(self) + -- Get all board data + self.boards = Boards:get_boards() + + -- Get all user data + self.users = Users:get_users() + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Page title + self.page_title = self.i18n("admin_panel") + + -- Verify Authorization + if self.session.name then + if not self.session.admin then + assert_error(false, "err_not_admin") + end + else + return + end + + -- Display creation form + if self.params.action == "create" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("create_user") + ) + self.user = self.params + return + end + + -- Display modification form + if self.params.action == "modify" then + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("modify_user") + ) + self.user = Users:get_user_by_id(self.params.user) + return + end + + -- Delete user + if self.params.action == "delete" then + local user = Users:get_user_by_id(self.params.user) + assert_error(Users:delete_user(user)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("deleted_user", { user.username }) + return + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.user" } + elseif self.params.action == "modify" then + return { render = "admin.user" } + elseif self.params.action == "delete" then + return { render = "admin.admin" } + end + end, + GET = function(self) + if not self.session.name then + return { render = "admin.login" } + elseif self.params.action == "create" then + return { render = "admin.user" } + elseif self.params.action == "modify" then + return { render = "admin.user" } + elseif self.params.action == "delete" then + return { render = "admin.success" } + end + end, + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Create new user + if self.params.create_user then + local sl = string.lower + + -- Validate user input + assert_valid(self.params, { + { "username", exists=true, max_length=255 }, + { "new_password", exists=true, equals=self.params.retype_password }, + { "retype_password", exists=true } + }) + + self.params.password = self.params.new_password + + -- Verify unique name + for _, user in ipairs(self.users) do + if sl(user.username) == sl(self.params.username) then + assert_error(false, "err_user_used") + end + end + + -- Create user + local user = assert_error(Users:create_user(self.params)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("created_user", { user.username }) + + return { render = "admin.success" } + end + + -- Modify user + if self.params.modify_user then + -- Validate user input + assert_valid(self.params, { + { "username", exists=true, max_length=255 }, + { "new_password", equals=self.params.retype_password }, + { "retype_password", } + }) + + local discard = { + "user", + "modify_user", + "ip", + "action", + "csrf_token", + "old_password", + "new_password", + "retype_password", + } + + local user = Users:get_user(self.params.username) + + -- Validate user + if #self.params.old_password > 0 then + -- Validate user input + assert_valid(self.params, { + { "new_password", exists=true }, + { "retype_password", exists=true } + }) + + -- TODO: verify user's old password in non-admin setting + + self.params.password = self.params.new_password + end + + -- Fill in board with new data + for k, param in pairs(self.params) do + user[k] = param + end + + -- Get rid of form trash + for _, param in ipairs(discard) do + user[param] = nil + end + + assert_error(Users:modify_user(user)) + + self.page_title = string.format( + "%s - %s", + self.i18n("admin_panel"), + self.i18n("success") + ) + self.action = self.i18n("modified_user", { user.username }) + + return { render = "admin.success" } + end + + return { redirect_to = self:url_for("web.admin.index") } + end +} diff --git a/website/backend/app/src/apps/web/boards.lua b/website/backend/app/src/apps/web/boards.lua new file mode 100755 index 0000000..fe8aad7 --- /dev/null +++ b/website/backend/app/src/apps/web/boards.lua @@ -0,0 +1,14 @@ +local lapis = require "lapis" +local r2 = require("lapis.application").respond_to +local app = lapis.Application() +app.__base = app +app.name = "web.boards." +app.path = "/board" +app.handle_404 = require "apps.web.internal.code_404" + +app:match("board", "/:uri_name(/page/:page)", r2(require "apps.web.boards.board")) +app:match("catalog", "/:uri_name/catalog", r2(require "apps.web.boards.catalog")) +app:match("archive", "/:uri_name/archive", require "apps.web.boards.archive") +app:match("thread", "/:uri_name/thread/:thread(#:anchor:id)", r2(require "apps.web.boards.thread")) + +return app diff --git a/website/backend/app/src/apps/web/boards/archive.lua b/website/backend/app/src/apps/web/boards/archive.lua new file mode 100755 index 0000000..ea86912 --- /dev/null +++ b/website/backend/app/src/apps/web/boards/archive.lua @@ -0,0 +1,76 @@ +local assert_error = require("lapis.application").assert_error +local capture = require "utils.capture" +local format = require "utils.text_formatter" +local Posts = require "models.posts" +local Threads = require "models.threads" + +return function(self) + + -- Get board + for _, board in ipairs(self.boards) do + if board.name == self.params.uri_name then + self.board = board + break + end + end + + -- Board not found + if not self.board then + return self:write({ redirect_to = self:url_for("web.pages.index") }) + end + + -- Get announcements + -- TODO: Consolidate these into a single call + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" }))) + local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name }))) + for _, announcement in ipairs(board_announcements) do + table.insert(self.announcements, announcement) + end + + -- Page title + self.page_title = string.format( + "/%s/ - %s", + self.board.name, + self.board.title + ) + + -- Nav links link to sub page if available + self.sub_page = "archive" + + -- Get threads + self.threads = assert_error(capture.get(self:url_for("api.boards.archived", { uri_name=self.params.uri_name }))) + + -- Get time + self.days = math.floor(self.board.archive_time / 24 / 60 / 60) + + -- Get stats + for _, thread in ipairs(self.threads) do + thread.op = Posts:get_thread_op(thread.id) + thread.replies = Posts:count_posts(thread.id) - 1 + thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=thread.op.post_id }) + + -- Process name + thread.op.name = thread.op.name or self.board.anon_name + + -- Process tripcode + thread.op.trip = thread.op.trip or "" + + -- Process comment + if thread.op.comment then + local comment = thread.op.comment + comment = format.sanitize(comment) + comment = format.spoiler(comment) + + if #comment > 110 then + comment = comment:sub(1,100) + comment = comment .. "..." + end + + thread.op.comment = comment + else + thread.op.comment = "" + end + end + + return { render = "archive" } +end diff --git a/website/backend/app/src/apps/web/boards/board.lua b/website/backend/app/src/apps/web/boards/board.lua new file mode 100755 index 0000000..e7d4afe --- /dev/null +++ b/website/backend/app/src/apps/web/boards/board.lua @@ -0,0 +1,242 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local csrf = require "lapis.csrf" +local capture = require "utils.capture" +local format = require "utils.text_formatter" +local generate = require "utils.generate" +local process = require "utils.request_processor" +local Posts = require "models.posts" + +return { + before = function(self) + + -- Get board + for _, board in ipairs(self.boards) do + if board.name == self.params.uri_name then + self.board = board + break + end + end + + -- Board not found + if not self.board or self.params.page and not tonumber(self.params.page) then + return self:write({ redirect_to = self:url_for("web.pages.index") }) + end + + -- Get announcements + -- TODO: Consolidate these into a single call + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" }))) + local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name }))) + for _, announcement in ipairs(board_announcements) do + table.insert(self.announcements, announcement) + end + + -- Page title + self.page_title = string.format( + "/%s/ - %s", + self.board.name, + self.board.title + ) + + -- Flag comments as required or not + self.comment_flag = self.board.thread_comment + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Current page + self.params.page = self.params.page or 1 + + -- Get threads + local response = assert_error(capture.get(self:url_for("api.boards.threads", { uri_name=self.params.uri_name, uri_page=self.params.page }))) + self.threads = response.threads + self.pages = response.pages + + -- Get posts + for _, thread in ipairs(self.threads) do + -- Get posts visible on the board index + thread.posts = Posts:get_index_posts(thread.id) + + -- Get hidden posts + thread.hidden = Posts:count_hidden_posts(thread.id) + + -- Get op + local op = thread.posts[#thread.posts] + if not op then + assert_error(false, { "err_orphaned", { thread.id } }) + end + + thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id }) + + -- Format comments + for _, post in ipairs(thread.posts) do + -- OP gets a thread tag + if post.post_id == op.post_id then + post.thread = post.post_id + end + + post.name = post.name or self.board.anon_name + post.reply = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="q", id=post.post_id }) + post.link = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="p", id=post.post_id }) + post.remix = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="r", id=post.post_id }) + post.timestamp = os.date("%Y-%m-%d (%a) %H:%M:%S", post.timestamp) + post.file_size = math.floor(post.file_size / 1024) + post.file_dimensions = "" + + if post.file_width > 0 and post.file_height > 0 then + post.file_dimensions = string.format(", %dx%d", post.file_width, post.file_height) + end + + if not post.file_duration or post.file_duration == "0" then + post.file_duration = "" + else + post.file_duration = string.format(", %s", post.file_duration) + end + + if post.file_path then + local name, ext = post.file_path:match("^(.+)(%..+)$") + ext = string.lower(ext) + + -- Get thumbnail URL + if post.file_type == "audio" then + if post == thread.posts[#thread.posts] then + post.thumb = self:format_url(self.static_url, "op_audio.png") + else + post.thumb = self:format_url(self.static_url, "post_audio.png") + end + elseif post.file_type == "image" then + if post.file_spoiler then + if post == thread.posts[#thread.posts] then + post.thumb = self:format_url(self.static_url, "op_spoiler.png") + else + post.thumb = self:format_url(self.static_url, "post_spoiler.png") + end + else + if ext == ".webm" or ext == ".svg" then + post.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png') + else + post.thumb = self:format_url(self.files_url, self.board.name, 's' .. post.file_path) + end + end + end + + post.file_path = self:format_url(self.files_url, self.board.name, post.file_path) + end + + -- Process comment + if post.comment then + local comment = post.comment + comment = format.sanitize(comment) + comment = format.quote(comment, self, self.board, post) + comment = format.green_text(comment) + comment = format.blue_text(comment) + comment = format.spoiler(comment) + comment = format.new_lines(comment) + post.comment = comment + else + post.comment = "" + end + end + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + return { render = "board"} + end, + GET = function() + return { render = "board" } + end, + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + local board_url = self:url_for("web.boards.board", { uri_name=self.board.name }) + + -- Submit new thread + if self.params.submit then + -- Validate user input + assert_valid(self.params, { + { "name", max_length=255 }, + { "subject", max_length=255 }, + { "options", max_length=255 }, + { "comment", max_length=self.text_size } + }) + + -- Validate post + local post = assert_error(process.create_thread(self.params, self.session, self.board)) + return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=post.post_id, anchor="p", id=post.post_id }) } + end + + -- Delete thread + if self.params.delete and self.params.thread_id then + -- Validate user input + assert_valid(self.params, { + { "post_id", exists=true } + }) + + -- Validate deletion + assert_error(process.delete_thread(self.params, self.session, self.board)) + return { redirect_to = board_url } + end + + -- Delete post + if self.params.delete and not self.params.thread_id then + -- Validate user input + assert_valid(self.params, { + { "post_id", exists=true } + }) + + -- Validate deletion + assert_error(process.delete_post(self.params, self.session, self.board)) + return { redirect_to = board_url } + end + + -- Report post + if self.params.report then + -- Validate user input + assert_valid(self.params, { + { "board", exists=true }, + { "post_id", exists=true } + }) + + -- Validate report + assert_error(process.report_post(self.params, self.board)) + return { redirect_to = board_url } + end + + -- Admin commands + if self.session.admin or self.session.mod then + -- Sticky thread + if self.params.sticky then + assert_error(process.sticky_thread(self.params, self.board)) + return { redirect_to = board_url } + end + + -- Lock thread + if self.params.lock then + assert_error(process.lock_thread(self.params, self.board)) + return { redirect_to = board_url } + end + + -- Save thread + if self.params.save then + assert_error(process.save_thread(self.params, self.board)) + return { redirect_to = board_url } + end + + -- Override thread + if self.params.override then + assert_error(process.override_thread(self.params, self.board)) + return { redirect_to = board_url } + end + + -- Ban user + if self.params.ban then + assert_error(process.ban_user(self.params, self.board)) + return { redirect_to = board_url } + end + end + + return { redirect_to = board_url } + end +} diff --git a/website/backend/app/src/apps/web/boards/catalog.lua b/website/backend/app/src/apps/web/boards/catalog.lua new file mode 100755 index 0000000..1e2eee8 --- /dev/null +++ b/website/backend/app/src/apps/web/boards/catalog.lua @@ -0,0 +1,128 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local process = require "utils.request_processor" +local capture = require "utils.capture" +local csrf = require "lapis.csrf" +local format = require "utils.text_formatter" +local generate = require "utils.generate" +local Posts = require "models.posts" + +return { + before = function(self) + + -- Get board + for _, board in ipairs(self.boards) do + if board.name == self.params.uri_name then + self.board = board + break + end + end + + -- Board not found + if not self.board then + return self:write({ redirect_to = self:url_for("web.pages.index") }) + end + + -- Get announcements + -- TODO: Consolidate these into a single call + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" }))) + local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name }))) + for _, announcement in ipairs(board_announcements) do + table.insert(self.announcements, announcement) + end + + -- Page title + self.page_title = string.format( + "/%s/ - %s", + self.board.name, + self.board.title + ) + + -- Nav links link to sub page if available + self.sub_page = "catalog" + + -- Flag comments as required or not + self.comment_flag = self.board.thread_comment + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Get threads + local response = assert_error(capture.get(self:url_for("api.boards.threads", { uri_name=self.params.uri_name }))) + self.threads = response.threads + + -- Get stats + for _, thread in ipairs(self.threads) do + thread.op = Posts:get_thread_op(thread.id) + thread.replies = Posts:count_posts(thread.id) - 1 + thread.files = Posts:count_files(thread.id) + thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=thread.op.post_id }) + + if thread.op.file_path then + local name, ext = thread.op.file_path:match("^(.+)(%..+)$") + ext = string.lower(ext) + + -- Get thumbnail URL + if thread.op.file_type == "audio" then + thread.op.thumb = self:format_url(self.static_url, "post_audio.png") + elseif thread.op.file_type == "image" then + if thread.op.file_spoiler then + thread.op.thumb = self:format_url(self.static_url, "post_spoiler.png") + else + if ext == ".webm" or ext == ".svg" then + thread.op.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png') + else + thread.op.thumb = self:format_url(self.files_url, self.board.name, 's' .. thread.op.file_path) + end + end + end + + thread.op.file_path = self:format_url(self.files_url, self.board.name, thread.op.file_path) + end + + -- Process comment + if thread.op.comment then + local comment = thread.op.comment + comment = format.sanitize(comment) + comment = format.spoiler(comment) + comment = format.new_lines(comment) + + if #comment > 260 then + comment = comment:sub(1, 250) .. "..." + end + + thread.op.comment = comment + else + thread.op.comment = "" + end + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + return { render = "catalog"} + end, + GET = function() + return { render = "catalog" } + end, + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + -- Submit new thread + if self.params.submit and not self.thread then + -- Validate user input + assert_valid(self.params, { + { "name", max_length=255 }, + { "subject", max_length=255 }, + { "options", max_length=255 }, + { "comment", max_length=self.text_size } + }) + + -- Validate post + local post = assert_error(process.create_thread(self.params, self.session, self.board)) + return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=post.post_id, anchor="p", id=post.post_id }) } + end + + return { redirect_to = self:url_for("web.boards.catalog", { uri_name=self.board.name }) } + end +} diff --git a/website/backend/app/src/apps/web/boards/thread.lua b/website/backend/app/src/apps/web/boards/thread.lua new file mode 100755 index 0000000..395df43 --- /dev/null +++ b/website/backend/app/src/apps/web/boards/thread.lua @@ -0,0 +1,242 @@ +local assert_error = require("lapis.application").assert_error +local assert_valid = require("lapis.validate").assert_valid +local csrf = require "lapis.csrf" +local capture = require "utils.capture" +local format = require "utils.text_formatter" +local generate = require "utils.generate" +local process = require "utils.request_processor" +local Posts = require "models.posts" + +return { + before = function(self) + + -- Get board + for _, board in ipairs(self.boards) do + if board.name == self.params.uri_name then + self.board = board + break + end + end + + -- Board not found + if not self.board then + return self:write({ redirect_to = self:url_for("web.pages.index") }) + end + + -- Get current thread data + local post = Posts:get(self.board.id, self.params.thread) + if not post then + return self:write({ redirect_to = self:url_for("web.boards.board", { uri_name=self.board.name }) }) + end + + self.thread = post:get_thread() + if not self.thread then + return self:write({ redirect_to = self:url_for("web.boards.board", { uri_name=self.board.name }) }) + end + + local op = self.thread:get_op() + if post.post_id ~= op.post_id then + return self:write({ redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="p", id=post.post_id }) }) + end + + -- Get announcements + -- TODO: Consolidate these into a single call + self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" }))) + local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name }))) + for _, announcement in ipairs(board_announcements) do + table.insert(self.announcements, announcement) + end + + -- Page title + self.page_title = string.format( + "/%s/ - %s", + self.board.name, + self.board.title + ) + + -- Flag comments as required or not + self.comment_flag = self.board.thread_comment + + -- Generate CSRF token + self.csrf_token = csrf.generate_token(self) + + -- Determine if we allow a user to upload a file + self.num_files = Posts:count_files(self.thread.id) + + -- Get posts + self.posts = self.thread:get_posts() + + -- Format comments + for i, post in ipairs(self.posts) do + -- OP gets a thread tag + if i == 1 then + post.thread = post.post_id + end + + post.name = post.name or self.board.anon_name + post.reply = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="q", id=post.post_id }) + post.link = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="p", id=post.post_id }) + post.timestamp = os.date("%Y-%m-%d (%a) %H:%M:%S", post.timestamp) + post.file_size = math.floor(post.file_size / 1024) + post.file_dimensions = "" + + if post.file_width > 0 and post.file_height > 0 then + post.file_dimensions = string.format(", %dx%d", post.file_width, post.file_height) + end + + if not post.file_duration or post.file_duration == "0" then + post.file_duration = "" + else + post.file_duration = string.format(", %s", post.file_duration) + end + + if post.file_path then + local name, ext = post.file_path:match("^(.+)(%..+)$") + ext = string.lower(ext) + + -- Get thumbnail URL + if post.file_type == "audio" then + if post == self.posts[1] then + post.thumb = self:format_url(self.static_url, "op_audio.png") + else + post.thumb = self:format_url(self.static_url, "post_audio.png") + end + elseif post.file_type == "image" then + if post.file_spoiler then + if post == self.posts[1] then + post.thumb = self:format_url(self.static_url, "op_spoiler.png") + else + post.thumb = self:format_url(self.static_url, "post_spoiler.png") + end + else + if ext == ".webm" or ext == ".svg" then + post.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png') + else + post.thumb = self:format_url(self.files_url, self.board.name, 's' .. post.file_path) + end + end + end + + post.file_path = self:format_url(self.files_url, self.board.name, post.file_path) + end + + -- Process comment + if post.comment then + local comment = post.comment + comment = format.sanitize(comment) + comment = format.quote(comment, self, self.board, post) + comment = format.green_text(comment) + comment = format.blue_text(comment) + comment = format.spoiler(comment) + comment = format.new_lines(comment) + post.comment = comment + else + post.comment = "" + end + end + end, + on_error = function(self) + self.errors = generate.errors(self.i18n, self.errors) + return { render = "thread"} + end, + GET = function() + return { render = "thread" } + end, + POST = function(self) + -- Validate CSRF token + csrf.assert_token(self) + + local board_url = self:url_for("web.boards.board", { uri_name=self.board.name }) + local thread_url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id }) + + -- Submit new post + if self.params.submit and self.thread then + -- Validate user input + assert_valid(self.params, { + { "thread", exists=true }, + { "name", max_length=255 }, + { "subject", max_length=255 }, + { "options", max_length=255 }, + { "comment", max_length=self.text_size } + }) + + -- Validate post + local post = assert_error(process.create_post(self.params, self.session, self.board, self.thread)) + return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="p", id=post.post_id }) } + end + + -- Delete thread + if self.params.delete and self.params.thread_id then + -- Validate user input + assert_valid(self.params, { + { "post_id", exists=true } + }) + + -- Validate deletion + assert_error(process.delete_thread(self.params, self.session, self.board)) + return { redirect_to = board_url } + end + + -- Delete post + if self.params.delete and not self.params.thread_id then + -- Validate user input + assert_valid(self.params, { + { "post_id", exists=true } + }) + + -- Validate deletion + assert_error(process.delete_post(self.params, self.session, self.board)) + return { redirect_to = thread_url } + end + + -- Report post + if self.params.report then + -- Validate user input + assert_valid(self.params, { + { "board", exists=true }, + { "post_id", exists=true } + }) + + -- Validate report + assert_error(process.report_post(self.params, self.board)) + return { redirect_to = thread_url } + end + + -- Admin commands + if self.session.admin or self.session.mod then + -- Sticky thread + if self.params.sticky then + assert_error(process.sticky_thread(self.params, self.board)) + return { redirect_to = thread_url } + end + + -- Lock thread + if self.params.lock then + assert_error(process.lock_thread(self.params, self.board)) + return { redirect_to = thread_url } + end + + -- Save thread + if self.params.save then + assert_error(process.save_thread(self.params, self.board)) + return { redirect_to = thread_url } + end + + -- Override thread + if self.params.override then + assert_error(process.override_thread(self.params, self.board)) + return { redirect_to = thread_url } + end + + -- Ban user + if self.params.ban then + assert_error(process.ban_user(self.params, self.board)) + return { redirect_to = thread_url } + end + + return { redirect_to = thread_url } + end + + return { redirect_to = thread_url } + end +} diff --git a/website/backend/app/src/apps/web/internal/check_auth.lua b/website/backend/app/src/apps/web/internal/check_auth.lua new file mode 100755 index 0000000..8711f4b --- /dev/null +++ b/website/backend/app/src/apps/web/internal/check_auth.lua @@ -0,0 +1,29 @@ +local Users = require "models.users" + +return function(self) + -- Prepare session names + self.session.names = self.session.names or {} + + -- Verify Authorization + if self.session.name then + local user = Users:get_user(self.session.name) + + if user then + user.password = nil + self.session.admin = user.admin + self.session.mod = user.mod + self.session.janitor = user.janitor + else + self.session.admin = nil + self.session.mod = nil + self.session.janitor = nil + end + else + self.session.admin = nil + self.session.mod = nil + self.session.janitor = nil + end + + -- Get IP from ngx + self.params.ip = self.req.headers["X-Real-IP"] or self.req.remote_addr +end diff --git a/website/backend/app/src/apps/web/internal/check_ban.lua b/website/backend/app/src/apps/web/internal/check_ban.lua new file mode 100755 index 0000000..bf69b7d --- /dev/null +++ b/website/backend/app/src/apps/web/internal/check_ban.lua @@ -0,0 +1,41 @@ +local assert_error = require("lapis.application").assert_error +local capture = require "utils.capture" + +return function(self) + -- MODS = FAGS + if self.session.admin or + self.session.mod or + self.session.janitor or + self.route_name == "admin" then + return + end + + -- Get list of bans by ip + local bans = assert_error(capture.get(self:url_for("api.bans.bans_ip", { uri_ip=self.params.ip }))) + + -- Get current board + local board = {} + if self.params.uri_name then + board = assert_error(capture.get(self:url_for("api.boards.board", { uri_name=self.params.uri_name }))) + end + + -- If you are banned, gtfo + for _, ban in ipairs(bans) do + if ban.board_id == 0 or + ban.board_id == board.id then + + -- Ban data + self.ip = ban.ip + self.reason = ban.reason or self.i18n("err_ban_reason") + self.expire = os.date("%Y-%m-%d (%a) %H:%M:%S", ban.time + ban.duration) + + -- Page title + self.page_title = self.i18n("ban_title") + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + return self:write({ render = "banned" }) + end + end +end diff --git a/website/backend/app/src/apps/web/internal/code_404.lua b/website/backend/app/src/apps/web/internal/code_404.lua new file mode 100755 index 0000000..5b545ad --- /dev/null +++ b/website/backend/app/src/apps/web/internal/code_404.lua @@ -0,0 +1,14 @@ +local Boards = require "models.boards" + +return function(self) + -- Get all board data + self.boards = Boards:get_boards() + + -- Page title + self.page_title = self.i18n("404") + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + return { render = "code_404", status = "404" } +end diff --git a/website/backend/app/src/apps/web/internal/config_site.lua b/website/backend/app/src/apps/web/internal/config_site.lua new file mode 100755 index 0000000..1e42595 --- /dev/null +++ b/website/backend/app/src/apps/web/internal/config_site.lua @@ -0,0 +1,45 @@ +local config = require("lapis.config").get() +local assert_error = require("lapis.application").assert_error +local i18n = require "i18n" +local lfs = require "lfs" +local capture = require "utils.capture" + +return function(self) + -- Set basic information + self.software = "Lapis-chan" + self.version = "1.2.5" + self.site_name = config.site_name + self.text_size = _G.text_size + + -- Get localization files + self.locales = {} + for file in lfs.dir("src/locale") do + local name, ext = string.match(file, "^(.+)%.(.+)$") + if ext == "lua" then + table.insert(self.locales, name) + end + end + + -- Set localization + if self.params.locale then + self.session.locale = self.params.locale + end + + i18n.setLocale(self.session.locale or "en") + i18n.loadFile("src/locale/en.lua") + if i18n.getLocale() ~= "en" then + i18n.loadFile("locale/" .. i18n.getLocale() .. ".lua") + end + self.i18n = i18n + + -- Get all boards + self.boards = assert_error(capture.get(self:url_for("api.boards.boards"))) + + -- Static + self.static_url = "/static/%s" + self.files_url = "/files/%s/%s" + + function self.format_url(_, pattern, ...) + return self:build_url(string.format(pattern, ...)) + end +end diff --git a/website/backend/app/src/apps/web/internal/install.lua b/website/backend/app/src/apps/web/internal/install.lua new file mode 100755 index 0000000..002786e --- /dev/null +++ b/website/backend/app/src/apps/web/internal/install.lua @@ -0,0 +1,333 @@ +local Users = require "models.users" +local Boards = require "models.boards" +local Pages = require "models.pages" +local lfs = require "lfs" +local validate = require("lapis.validate").validate +local faq = [[ +
+
    +
  1. What is Lapis-chan?
  2. +
  3. What is Lapchan?
  4. +
  5. What should I know before posting?
  6. +
  7. What are the basics?
  8. +
  9. How do I post anonymously?
  10. +
  11. Do I have to post an image?
  12. +
  13. How do I quote a post?
  14. +
  15. What is a tripcode?
  16. +
  17. Can I mark an image as a spoiler?
  18. +
  19. What are post options?
  20. +
  21. How can I interact with posts?
  22. +
  23. What types of boards are supported?
  24. +
+
+
+
+

What is Lapis-chan?

+

+ Lapis-chan is an open source imageboard web application written in Lua + using the Lapis web framework. +

+
+
+

What is Lapchan?

+

+ Lapchan is a website that runs the latest version of Lapis-chan. It is + both used as a small community and a testing platform for new and + experimental features. +

+
+
+

What Should I Know Before Posting?

+

+ Before posting, you should read the rules for whichever board you want to + post in. If you break the rules, your post may be deleted and you may + also earn yourself a temporary or permanent ban from the board or entire + website. Please read the rules! +

+
+
+

What are the Basics?

+

+ In general, "blue" boards are considered safe for work (SFW) and "red" + boards are considered not safe for work (NSFW). The definition of + work-safety is often loose, but in general it means there shouldn't be + any direct pornographic material on a blue board. This may differ from + site to site, but Lapis-chan by default offers blue and red themes that + are nearly identical to 4chan's themes. New themes are expected in later + releases. +

+

+ It is also worth noting that chan culture can be both friendly and + abrasive. More often than not, users will be posting anonymously and are + able to speak freely because if this. Be prepared for the best and worst + of society when interacting with others in an anonymous forum. +

+
+
+

How Do I Post Anonymously?

+

+ By default, all users are anonymous to each other. By leaving the "Name" + field empty in a post, your name will simply be fille din with the + board's default name. Identifiable information such as your IP address is + recorded to the Lapis-chan database when you post for legal reasons, but + all posts are permanently purged after some time unless otherwise noted, + including your IP address and any other information attached to your + post. +

+
+
+

Do I Have to Post an Image?

+

+ Some boards require you to post an image or a comment, others do not. By + default, Lapis-chan will place "(Required)" in or near a field that + requires data. Currently, Lapis-chan's only optionally required data + include a comment and an image. +

+
+
+

How Do I Quote a Post?

+

+ To quote (and link to) another post, simply type ">>" followed by + the post number (e.g. >>2808). To quote a post that is on a + different board, You must type ">>>" follow by a slash, the + name of the board, another slash, and then the post number + (e.g. >>>/a/2808). +

+
+
+

What is a Tripcode?

+

+ A tripcode is a uniquely identifiable hash attached to the end of your + name, or in lieu of a name. It is a completely optional feature that + allows users to de-anonymize if they so choose. Some boards benefit from + de-anonymization such as content-creation boards where being named can + help get your content seen and recognized. +

+

+ Tripcodes come in two sizes: insecure and secure. Insecure tripcodes use + a weak hashing method that allows users to game the algorithm to generate + a hash that reads out something similar to what they want. A secure + tripcode uses a very secure hashing algorithm and a server-specific + secret token that is not gameable, but also significantly more difficult + to impersonate. +

+

+ To use an insecure tripcode, place a hash ("#") sign at the end of your + name (or leave a name out entirely) and type your password after the + hash. To use a secure tripcode, simply use two hashes instead of one + (e.g. lapchan#insecure, lapchan##secure, #nameless, ##nameless). +

+
+
+

Can I Mark an Image as a Spoiler?

+

+ Yes. When you upload an image, there should be a check box beside the + file input field. Checking that box will replace the thumbnail of your + image with a spoiler image. +

+

+ You can also tag text within your post as a spoiler by writing your text + with [spoiler]a spoiler tag[/spoiler]. +

+
+
+

What Are Post Options?

+

+ Post options are optional features you can use to modify how your post + affects the thread or board you are posting in. To apply an options, + simply type the option code into the options field in your post. + Currently, the options available include: +

+
    +
  • sage - Do not bump the thread with your post.
  • +
+
+
+

How Can I Interact With Posts?

+

+ To interact with a post, click on the menu icon ("▶") on the left of the + post. The menu currently has the following interactions: +

+
    +
  • + Report Post - Report a post to moderators that you believe is breaking + the rules of the board. +
  • +
  • + Delete Post - Delete your own post. Lapis-chan saves a unique password + to your user session when you make your first post and will allow you + to delete any post you make as long as you are using the same session. +
  • +
  • + Remix Image - Draw boards have a remix feature that allows you to copy + the image from a post into the drawing canvas and draw on top of it. + You can then post your new image in the thread to show off your + updated image. +
  • +
+
+
+

What Types of Boards are Supported?

+

+ Lapis-chan has several different types of boards with more planned in the + future. Currently, Lapis-chan supports the following boards: +

+
    +
  • + Image boards - Upload images and chat with other people about various + topics or sub-topics. Common image boards include discussing your + favourite TV shows, video games, or characters within. +
  • +
  • + Text boards - Strictly text. Common text boards include discussing + latest events, breaking news, politics, or writing stories. +
  • +
  • + Draw boards - Upload, draw, and remix images. Common draw boards + include art critiquing and art remixing. +
  • +
+
+
+]] + +local success = [[ +

+ Congratulations! Lapis-chan is now installed! Please rename or delete the + `install.lua` file to see your new board. Visit "/admin" to get started! +

+

Thank you for installing Lapis-chan! <3

+]] + +return { + GET = function(self) + self.page_title = "Install Lapis-chan" + self.board = { theme = "yotsuba_b" } + + -- Do we already have data? + local users = Users:get_users() + local boards = Boards:get_boards() + local pages = Pages:get_pages() + + -- We did it! + if #users > 0 and #boards > 0 and #pages > 0 then + return success + end + + -- Get list of themes + self.themes = {} + for file in lfs.dir("."..self.styles_url) do + local name, ext = string.match(file, "^(.+)(%..+)$") + if name ~= "reset" and + name ~= "posts" and + name ~= "style" and + name ~= "tegaki" and + ext == ".css" then + table.insert(self.themes, name) + end + end + + return { render = "install" } + end, + POST = function(self) + self.page_title = "Install Lapis-chan" + self.board = { theme = "yotsuba_b" } + + -- Do we already have data? + local users = Users:get_users() + local boards = Boards:get_boards() + local pages = Pages:get_pages() + + -- We did it! + if #users > 0 and #boards > 0 and #pages > 0 then + return success + end + + local errs = validate(self.params, { + { "user_username", exists=true, max_length=255 }, + { "user_password", min_length=4, max_length=255 }, + { "name", exists=true, max_length=10 }, + { "title", min_length=2, max_length=255 }, + { "subtext", max_length=255 }, + { "rules" }, + { "ban_message", max_length=255 }, + { "anon_name", max_length=255 }, + { "theme", exists=true }, + { "pages", exists=true }, + { "threads_per_page", exists=true }, + { "thread_file_limit", exists=true }, + { "post_limit", exists=true }, + { "thread_file", exists=true }, + { "thread_comment", exists=true }, + { "post_file", exists=true }, + { "post_comment", exists=true }, + { "text_only", exists=true }, + { "filetype_image", exists=true }, + { "filetype_audio", exists=true }, + { "draw", exists=true }, + { "archive", exists=true }, + { "archive_time", exists=true }, + { "group", exists=true } + }) + + local out + if errs then + out = "
\n" + for _, err in ipairs(errs) do + out = out .. "

" .. err .. "

\n" + end + out = out .. [[ +
+ +
+
+ ]] + end + + if out then + return out + end + + -- Add new user + Users:create_user { + username = self.params.user_username, + password = self.params.user_password, + admin = true, + mod = false, + janitor = false + } + + -- Add new board + Boards:create_board { + name = self.params.name, + title = self.params.title, + subtext = self.params.subtext, + rules = self.params.rules, + anon_name = self.params.anon_name, + theme = self.params.theme, + posts = 0, + pages = self.params.pages, + threads_per_page = self.params.threads_per_page, + text_only = self.params.text_only, + filetype_image = self.params.filetype_image, + filetype_audio = self.params.filetype_audio, + draw = self.params.draw, + thread_file = self.params.thread_file, + thread_comment = self.params.thread_comment, + thread_file_limit = self.params.thread_file_limit, + post_file = self.params.post_file, + post_comment = self.params.post_comment, + post_limit = self.params.post_limit, + archive = self.params.archive, + archive_time = self.params.archive_time * 24 * 60 * 60, + group = self.params.group + } + + -- Add FAQ page + Pages:create_page { + title = "Frequently Asked Questions", + slug = "faq", + content = faq + } + end +} diff --git a/website/backend/app/src/apps/web/pages.lua b/website/backend/app/src/apps/web/pages.lua new file mode 100755 index 0000000..54d4547 --- /dev/null +++ b/website/backend/app/src/apps/web/pages.lua @@ -0,0 +1,14 @@ +local lapis = require "lapis" +local app = lapis.Application() +app.__base = app +app.name = "web.pages." +app.handle_404 = require "apps.web.internal.code_404" + +app:match("index", "/", require "apps.web.pages.index") +app:match("c404", "/404", require "apps.web.internal.code_404") -- FIXME: remove this route +app:match("faq", "/faq", require "apps.web.pages.rules") -- FIXME: need a faq page +app:match("rules", "/rules", require "apps.web.pages.rules") +app:match("logout", "/logout", require "apps.web.pages.logout") +app:match("page", "/:page", require "apps.web.pages.page") + +return app diff --git a/website/backend/app/src/apps/web/pages/index.lua b/website/backend/app/src/apps/web/pages/index.lua new file mode 100755 index 0000000..64c32e4 --- /dev/null +++ b/website/backend/app/src/apps/web/pages/index.lua @@ -0,0 +1,10 @@ +return function(self) + + -- Page title + self.page_title = self.i18n("index") + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + return { render = "index" } +end diff --git a/website/backend/app/src/apps/web/pages/logout.lua b/website/backend/app/src/apps/web/pages/logout.lua new file mode 100755 index 0000000..b7d0ccd --- /dev/null +++ b/website/backend/app/src/apps/web/pages/logout.lua @@ -0,0 +1,9 @@ +return function(self) + -- Logout + self.session.name = nil + self.session.admin = nil + self.session.mod = nil + self.session.janitor = nil + + return { redirect_to = self:url_for("web.pages.index") } +end diff --git a/website/backend/app/src/apps/web/pages/page.lua b/website/backend/app/src/apps/web/pages/page.lua new file mode 100755 index 0000000..36add7f --- /dev/null +++ b/website/backend/app/src/apps/web/pages/page.lua @@ -0,0 +1,28 @@ +local Boards = require "models.boards" +local Pages = require "models.pages" +local markdown = require "markdown" + +return function(self) + -- Get all board data + self.boards = Boards:get_boards() + + -- Get page + self.page = Pages:get_page(self.params.page) + + if not self.page then + return self:write({ redirect_to = self:url_for("web.pages.c404") }) + end + + -- Page title + self.page_title = self.page.title + + -- Markdown + if self.page.content then + self.page.content = markdown(self.page.content) + end + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + return { render = "page" } +end diff --git a/website/backend/app/src/apps/web/pages/rules.lua b/website/backend/app/src/apps/web/pages/rules.lua new file mode 100755 index 0000000..f45e50e --- /dev/null +++ b/website/backend/app/src/apps/web/pages/rules.lua @@ -0,0 +1,19 @@ +return function(self) + + -- Page title + self.page_title = self.i18n("rules") + + -- Display a theme + self.board = { theme = "yotsuba_b" } + + for _, board in ipairs(self.boards) do + board.url = self:url_for("web.boards.board", { board=board.name }) + if board.rules then + board.rules = _G.markdown(board.rules) + else + board.rules = "" + end + end + + return { render = "rules" } +end diff --git a/website/backend/app/src/locale/en.lua b/website/backend/app/src/locale/en.lua new file mode 100755 index 0000000..c88a150 --- /dev/null +++ b/website/backend/app/src/locale/en.lua @@ -0,0 +1,236 @@ +return { en = { + + --==[[ Navigation ]]==-- + + archive = "Archive", + bottom = "Bottom", + catalog = "Catalog", + index = "Index", + refresh = "Refresh", + ["return"] = "Return", + return_board = "Return to board", + return_index = "Return to index", + return_thread = "Return to thread", + top = "Top", + + --==[[ Error Messages ]]==-- + + -- Controller error messages + err_ban_reason = "None given.", + err_board_used = "Board name already in use.", + err_not_admin = "You are not an administrator.", + err_orphaned = "Thread No.%s has been orphaned.", + err_slug_used = "Page slug already in use.", + err_user_used = "Username already in use.", + + -- Model error messages + err_contribute = "You must post either a comment or a file.", + err_locked_thread = "Thread No.%s is locked.", + err_no_files = "Files are not accepted on this board.", + + err_comment_post = "Comments are required to post on this board.", + err_comment_thread = "Comments are required to post a thread on this board.", + + err_create_ann = "Could not create announcement: %s.", + err_create_ban = "Could not ban IP: %s.", + err_create_board = "Could not create board: /%s/ - %s.", + err_create_page = "Could not create page: /%s/ - %s.", + err_create_post = "Could not submit post.", + err_create_report = "Could not report post No.%s.", + err_create_thread = "Could not create new thread.", + + err_delete_board = "Could not delete board: /%s/ - %s.", + err_delete_post = "Could not delete post No.%s.", + err_create_user = "Could not create user: %s.", + + err_file_exists = "File already exists on this board.", + err_file_limit = "Thread No.%s is at its file limit.", + err_file_post = "Files are required to post on this board.", + err_file_thread = "Files are required to post a thread on this board.", + + err_invalid_board = "Invalid board: /%s/.", + err_invalid_ext = "Invalid filetype: %s.", + err_invalid_image = "Invalid image data.", + err_invalid_post = "Post No.%s is not a valid post.", + err_invalid_user = "Invalid username or password.", + + --==[[ 404 ]]==-- + + ["404"] = "404 - Page Not Found", + + --==[[ Administration ]]==-- + + -- General + admin_panel = "Admin Panel", + administrator = "Administrator", + announcement = "Announcement", + archive_days = "Days to Archive Threads", + archive_pruned = "Archive Pruned Threads", + board = "Board", + board_group = "Board Group", + board_title = "Board Title", + bump_limit = "Bump Limit", + content_md = "Content (Markdown)", + default_name = "Default Name", + draw_board = "Draw Board", + file = "File", + file_limit = "Thread File Limit", + filetype_image = "Allow Image Files", + filetype_audio = "Allow Audio Files", + global = "Global", + index_boards = "Current Boards", + janitor = "Janitor", + login = "Login", + logout = "Logout", + moderator = "Moderator", + num_pages = "Active Pages", + num_threads = "Threads per Page", + password = "Password", + password_old = "Old Password", + password_retype = "Retype Password", + post_comment_required = "Post Comment Required", + post_file_required = "Post File Required", + regen_thumb = "Regenerate Thumbnails", + reply = "Reply", + rules = "Rules", + name = "Name", + subtext = "Subtext", + success = "Success", + text_board = "Text Board", + theme = "Theme", + thread_comment_required = "Thread Comment Required", + thread_file_required = "Thread File Required", + slug = "Slug", + username = "Username", + yes = "Yes", + no = "No", + + -- Announcements + create_ann = "Create Announcement", + modify_ann = "Modify Announcement", + delete_ann = "Delete Announcement", + created_ann = "You have successfully created announcement: %s.", + modified_ann = "You have successfully modified announcement: %s.", + deleted_ann = "You have successfully deleted announcement: %s.", + + -- Boards + create_board = "Create Board", + modify_board = "Modify Board", + delete_board = "Delete Board", + created_board = "You have successfully created board: /%s/ - %s.", + modified_board = "You have successfully modified board: /%s/ - %s.", + deleted_board = "You have successfully deleted board: /%s/ - %s.", + + -- Pages + create_page = "Create Page", + modify_page = "Modify Page", + delete_page = "Delete Page", + created_page = "You have successfully created page: /%s/ - %s.", + modified_page = "You have successfully modified page: /%s/ - %s.", + deleted_page = "You have successfully deleted page: /%s/ - %s.", + + -- Reports + view_report = "View Report", + delete_report = "Delete Report", + deleted_report = "You have successfully deleted report: %s.", + + -- Users + create_user = "Create User", + modify_user = "Modify User", + delete_user = "Delete User", + created_user = "You have successfully created user: %s.", + modified_user = "You have successfully modified user: %s.", + deleted_user = "You have successfully deleted user: %s.", + + --==[[ Archive ]]==-- + + arc_display = "Displaying %{n_thread} expired %{p_thread} from the past %{n_day} %{p_day}", + arc_number = "No.", + arc_name = "Name", + arc_excerpt = "Excerpt", + arc_replies = "Replies", + arc_view = "View", + + --==[[ Ban ]]==-- + + ban_title = "Banned!", + ban_reason = "You have been banned for the following reason:", + ban_expire = "Your ban will expire on %{expire}.", + ban_ip = "According to our server, your IP is: %{ip}.", + + --==[[ Catalog ]]==-- + + cat_stats = "R: %{replies} / F: %{files}", + + --==[[ Copyright ]]==-- + + copy_software = "Powered by %{software} %{version}", + copy_download = "Download from %{github}", + + --==[[ Forms ]]==-- + + form_ban = "Ban User", + form_ban_display = "Display Ban", + form_ban_board = "Local Ban", + form_ban_reason = "Reason for ban", + form_ban_time = "Length of time (in days) to ban user", + form_clear = "Clear", + form_delete = "Delete Post", + form_draw = "Draw", + form_lock = "Lock Thread", + form_override = "Unlimited Files", + form_readme = "Please read the [%{rules}] and [%{faq}] before posting.", + form_remix = "Remix Image", + form_report = "Report Post", + form_required = "required field", + form_save = "Save Thread", + form_sticky = "Sticky Thread", + form_submit = "Submit Post", + form_submit_name = "Name", + form_submit_name_help = "Give yourself a name and/or tripcode (optional)", + form_submit_subject = "Subject", + form_submit_subject_help = "Define the topic of discussion (optional)", + form_submit_options = "Options", + form_submit_options_help = "sage: post without bumping thread (more to come soon) (optional)", + form_submit_comment = "Comment", + form_submit_comment_help = "Contribute to the discussion (or not)", + form_submit_file = "File", + form_submit_file_help = "Upload a file", + form_submit_draw = "Draw", + form_submit_draw_help = "Draw or remix an image", + form_submit_spoiler = "Spoiler", + form_submit_spoiler_help = "Replace thumbnail with a spoiler-safe image", + form_submit_mod = "Moderator", + form_submit_mod_help = "Flag this thread", + form_width = "Width", + form_height = "Height", + + --==[[ Posts ]]==-- + + post_link = "Link to this post", + post_lock = "Thread is locked", + post_hidden = "%{n_post} %{p_post} and %{n_file} %{p_file} omitted. %{click} to view.", + post_override = "Thread accepts unlimited files", + post_reply = "Reply to this post", + post_sticky = "Thread is stickied", + post_save = "Thread will not be pruned", + + --==[[ Plurals ]]==-- + + days = { + one = "day", + other = "days" + }, + files = { + one = "file", + other = "files" + }, + posts = { + one = "post", + other = "posts" + }, + threads = { + one = "thread", + other = "threads" + }, +}} diff --git a/website/backend/app/src/locale/fr.lua b/website/backend/app/src/locale/fr.lua new file mode 100755 index 0000000..654f586 --- /dev/null +++ b/website/backend/app/src/locale/fr.lua @@ -0,0 +1,234 @@ +return { fr = { + + --==[[ Navigation ]]==-- + + archive = "Archive", + bottom = "Bas", + catalog = "Catalogue", + index = "Index", + refresh = "Rafraîchir", + ["return"] = "Retourner", + return_board = "Retourner au babillard", + return_index = "Retourner à l'index", + return_thread = "Retourner au fil de discussion", + top = "Haut", + + --==[[ Error Messages ]]==-- + + -- Controller error messages + err_ban_reason = "Aucune Reçue.", + err_board_used = "Le nom du Babillard est déjà utilisé.", + err_not_admin = "Vous n'êtes pas un administrateur.", + err_orphaned = "Le Fil de Discussion nº%s est orphelin.", + err_slug_used = "Le slug de la page est déjà utilisée.", + err_user_used = "Le nom d'utilisateur est dèja utilisé.", + + -- Model error messages + err_contribute = "Vous devez poster un commentaire ou un fichier.", + err_locked_thread = "Le fil de discussion No.%s est verrouillé.", + err_no_files = "Les fichiers ne sont pas acceptés sur ce babillard.", + + err_comment_post = "Un commentaire est requis pour poster sur ce babillard.", + err_comment_thread = "Un commentaire est requis pour poster un fil de discussion sur ce babillard.", + + err_create_ann = "Impossible de créer un annoncement: %s.", + err_create_ban = "Impossible de bannir l'IP: %s.", + err_create_board = "Impossible de créer le babillard: /%s/ - %s.", + err_create_page = "Impossible de créer la page: /%s/ - %s.", + err_create_post = "Impossible de soumettre le poste.", + err_create_report = "Impossible de reporter le post No.%s.", + err_create_thread = "Impossible de créer un nouveau fil de discussion.", + + err_delete_board = "Impossible de supprimer le babillard: /%s/ - %s.", + err_delete_post = "Impossible de supprimer le poste No.%s.", + err_create_user = "Impossible de créer l'utilisateur: %s.", + + err_file_exists = "Le fichier existe déjà sur ce babillard.", + err_file_limit = "Le fil de discussion No.%s a atteint sa limite de fichier.", + err_file_post = "Un fichier est requis pour poster sur ce babillard.", + err_file_thread = "Un fichier est requis pour poster un fil de discussion sur ce babillard.", + + err_invalid_board = "Babillard invalide: /%s/.", + err_invalid_ext = "Type de fichier invalide: %s.", + err_invalid_image = "Données d'image invalides.", + err_invalid_post = "Le poste No.%s n'est pas valide.", + err_invalid_user = "Nom d'utilisateur ou mot de passe invalide.", + + --==[[ 404 ]]==-- + + ["404"] = "404 - Page Introuvable", + + --==[[ Administration ]]==-- + + -- General + admin_panel = "Panneau Administratif", + administrator = "Administrateur", + announcement = "Annoncement", + archive_days = "Nombre de jours à garder les Fils de Discussion dans l'Archive", + archive_pruned = "Archiver les Fils de Discussion Réduites", + board = "Babillard", + board_group = "Group de babillard", + board_title = "Nom de Babillard", + bump_limit = "Limite pour remonter un fil de discussion", + content_md = "Contenu (Markdown)", + default_name = "Nom par Défaut", + draw_board = "Babillard à Dessin", + file = "Fichier", + file_limit = "Limite de fichiers dans un Fil de Discussion", + global = "Global", + index_boards = "Babillards Présent", + janitor = "Concierge", + login = "Connexion", + logout = "Déconnexion", + moderator = "Modérateur", + num_pages = "Pages Actives", + num_threads = "Fils de Discussion par Page", + password = "Mot de Passe", + password_old = "Ancient Mot de Passe", + password_retype = "Confirmer le Mot de Passe", + post_comment_required = "Exiger un commentaire pour pouvoir poster", + post_file_required = "Exiger un Fichier pour pouvoir poster", + regen_thumb = "Régénérer les Miniatures", + reply = "Répondre", + rules = "Les Règles", + name = "Nom", + subtext = "Sous-texte", + success = "Succès", + text_board = "Babillard à Texte", + theme = "Thème", + thread_comment_required = "Exiger un Commentaire ", + thread_file_required = "Exiger un Fil de Discussion", + slug = "Slug", + username = "Nom d'utilisateur", + yes = "Oui", + no = "Non", + + -- Announcements + create_ann = "Créer un Annoncement", + modify_ann = "Modifier un Annoncement", + delete_ann = "Supprimer un Annoncement", + created_ann = "Création de l'annoncement: %s réussie.", + modified_ann = "Modification de l'annoncement: %s réussie.", + deleted_ann = "Suppression de l'annoncement: %s réussie.", + + -- Boards + create_board = "Créer un Babillard", + modify_board = "Modifier un Babillard", + delete_board = "Supprimer un Babillard", + created_board = "Création du Babillard: /%s/ - %s réussie.", + modified_board = "Modification du Babillard : /%s/ - %s réussie.", + deleted_board = "Suppression du Babillard : /%s/ - %s réussie.", + + -- Pages + create_page = "Créer une Page", + modify_page = "Modifier une Page", + delete_page = "Supprimer une Page", + created_page = "Création de la page : /%s/ - %s réussie.", + modified_page = "Modification de la page : /%s/ - %s réussie.", + deleted_page = "Suppression de la page : /%s/ - %s réussie.", + + -- Reports + view_report = "Afficher un Rapport", + delete_report = "Supprimer un Rapport", + deleted_report = "Suppression du Rapport: %s réussie.", + + -- Users + create_user = "Créer un Utilisateur", + modify_user = "Modifier un Utilisateur", + delete_user = "Supprimer un Utilisateur", + created_user = "Création de l'utilisateur: %s réussie.", + modified_user = "Modification de l'utilisateur: %s réussie.", + deleted_user = "Suppression de l'utilisateur: %s réussie.", + + --==[[ Archive ]]==-- + + arc_display = "Affichage de %{n_thread} %{p_thread} expirés depuis %{n_day} %{p_day} ", + arc_number = "nº", + arc_name = "Nom", + arc_excerpt = "Extrait", + arc_replies = "Réponses", + arc_view = "Afficher", + + --==[[ Ban ]]==-- + + ban_title = "Banni!", + ban_reason = "Vous avez été banni pour la raison suivante:", + ban_expire = "Votre ban expirera le %{expire}.", + ban_ip = "Selon avec notre serveur, votre IP est: %{ip}.", + + --==[[ Catalog ]]==-- + + cat_stats = "R: %{replies} / F: %{files}", + + --==[[ Copyright ]]==-- + + copy_software = "Réalisé avec %{software} %{version}", + copy_download = "Télécharger à partir de %{github}", + + --==[[ Forms ]]==-- + + form_ban = "Bannir l'Utilisateur", + form_ban_display = "Afficher le Ban", + form_ban_board = "Ban Local", + form_ban_reason = "Raison du ban", + form_ban_time = "Durée (en jours) à bannir l'utilisateur", + form_clear = "Effacer", + form_delete = "Supprimer le Poste", + form_draw = "Dessiner", + form_lock = "Verrouiller le Fil de Discussion", + form_override = "Fichiers Illimités", + form_readme = "Veuillez lire [%{rules}] et la [%{faq}] avant de poster.", + form_remix = "Remixer l'Image", + form_report = "Reporter le Poste", + form_required = "Champ Requis", + form_save = "Épargner le Fil de Discussion", + form_sticky = "Épingler le Fil de Discussion", + form_submit = "Soumettre le Poste", + form_submit_name = "Nom", + form_submit_name_help = "Donnez-vous un nom , un tripcode ou les deux (facultatif)", + form_submit_subject = "Sujet", + form_submit_subject_help = "Définir le sujet de la discussion (facultatif)", + form_submit_options = "Options", + form_submit_options_help = "Sage: poster sans faire monter le fil de discussion (À venir) (facultatif)", + form_submit_comment = "Commentaire", + form_submit_comment_help = "Contribuer à la discussion (ou non)", + form_submit_file = "Fichier", + form_submit_file_help = "Télécharger un fichier", + form_submit_draw = "Dessiner", + form_submit_draw_help = "Dessiner ou remixer une image", + form_submit_spoiler = "Spoiler", + form_submit_spoiler_help = "Replacer la miniature avec une image sans spoiler", + form_submit_mod = "Modérateur", + form_submit_mod_help = "Mettre un drapeau sur ce fil de discussion", + form_width = "Largeur", + form_height = "Hauteur", + + --==[[ Posts ]]==-- + + post_link = "Lier à ce poste", + post_lock = "Le fil de discussion est verrouillé", + post_hidden = "%{n_post} %{p_post} et %{n_file} %{p_file} omis. %{click} pour afficher.", + post_override = "Ce fil de discussion accepte un nombre illimité de fichiers", + post_reply = "Répondre à ce poste", + post_sticky = "Le fil de discussion est épinglé", + post_save = "Le fil de discussion ne sera pas enlevé automatiquement", + + --==[[ Plurals ]]==-- + + days = { + one = "jour", + other = "jours" + }, + files = { + one = "fichier", + other = "fichiers" + }, + posts = { + one = "poste", + other = "postes" + }, + threads = { + one = "fil de discussion", + other = "fils de discussion" + }, +}} diff --git a/website/backend/app/src/locale/phpceo.lua b/website/backend/app/src/locale/phpceo.lua new file mode 100755 index 0000000..8f399d9 --- /dev/null +++ b/website/backend/app/src/locale/phpceo.lua @@ -0,0 +1,234 @@ +return { phpceo = { + + --==[[ Navigation ]]==-- + + archive = "Gallery Of Our Greatness", + bottom = "Best For Last", + catalog = "Products And Services", + index = "Home Page", + refresh = "Experience A Rejuvinating Meeting", + ["return"] = "Reconsider", + return_board = "Reconsider that product", + return_index = "Reconsider that home page", + return_thread = "Reconsider that email chain", + top = "I'm The Best", + + --==[[ Error Messages ]]==-- + + -- Controller error messages + err_ban_reason = "Hit 'enter' too early.", + err_board_used = "That product name is in use, and we don't want to spend money on lawyers.", + err_not_admin = "Call HR.", + err_orphaned = "I accidentally lost the email chain about %s, do you know where it went?", + err_slug_used = "SEO CONFLICT, GOOGLE GROWTHHACK ERROR.", + err_user_used = "You can't be Brandon, *I'm* Brandon!", + + -- Model error messages + err_contribute = "Your argument needs more pictures or more words. I can't decide which. I'm fickle like that.", + err_locked_thread = "Email chain about %s is locked, I blame Outlook.", + err_no_files = "The cloud is busy, it does not want your attachments.", + + err_comment_post = "Your pitch is great, except I don't know what you're talking about. Use your words.", + err_comment_thread = "Did you seriously just try to send an email without a body? At least put in your signature. Have some pride, Intern.", + + err_create_ann = "I couldn't stop yelling long enough to say: %s.", + err_create_ban = "These lottery numbers didn't work: %s.", + err_create_board = "I don't know how to make the product: /%s/ - %s.", + err_create_page = "Pages? No, we're paperless now, we don't need: /%s/ - %s.", + err_create_post = "I didn't read your email, let's set up a meeting in a few minutes where you read it to me.", + err_create_report = "I don't see anything wrong with an email about %s and I have no inclination to in my life.", + err_create_thread = "The reply button is broke! This is effecting production.", + + err_delete_board = "I tried to delete those emails about: /%s/ - but I just couldn't %s.", + err_delete_post = "I clicked the flag, did that delete %s? No? Well what else am I supposed to do?", + err_create_user = "I don't know who %s is and frankly I don't give a big enough damn to learn.", + + err_file_exists = "Johnson! I already have that spreadsheet! I think. Probably.", + err_file_limit = "THE CLOUD IS FULL, IF YOU SUBMIT ANYTHING ELSE TO %s THE INTERNET WILL GO DOWN.", + err_file_post = "If you want my attention you better use something other than words because I sure as hell ain't reading anything that comes across this desk today.", + err_file_thread = "This email chain said 'post your favorite confidential documents' but you forgot to attach any. Try that one again.", + + err_invalid_board = "CORRUPT EMAIL: /%s/.", + err_invalid_ext = "Do I look like I know what a %s is?", + err_invalid_image = "It looks like you sent a picture but what I got was a digital clown.", + err_invalid_post = "Whatever %s was about, I've decided to politely disregard it as 'wrong opinion'.", + err_invalid_user = "I don't know who you are or what you're doing. I'm calling the police.", + + --==[[ 404 ]]==-- + + ["404"] = "404 - That's 3 better than a 401k!", + + --==[[ Administration ]]==-- + + -- General + admin_panel = "CEO DASHBOARD", + administrator = "CEO", + announcement = "Important things that come out of my mouth", + archive_days = "Days to Archive Threads", + archive_pruned = "Gas these.", + board = "Chain", + board_group = "Chain Group", + board_title = "Email Subject", + bump_limit = "Burp Excusal Tolerance", + content_md = "Markers", + default_name = "What is this?", + draw_board = "MSPAINT but for the Internet", + file = "Datum gap", + file_limit = "Maximum Cloud Precipitation Ratio", + global = "Everyone Has To Deal With", + index_boards = "Product Selection", + janitor = "Unpaid Intern", + login = "Clock In", + logout = "Clock Out", + moderator = "Enforcer", + num_pages = "Reasons this company is great", + num_threads = "How much I can stand of this", + password = "Digital Hash Salt", + password_old = "That old thing.", + password_retype = "Do it again, it'll be funny.", + post_comment_required = "Need Context", + post_file_required = "Insert Meme", + regen_thumb = "Rectify Pixels", + reply = "Interject", + rules = "Things That Don't Apply To Me", + name = "Bob", + subtext = "sub-who?", + success = "Me, The Physical Embodiment of Greatness", + text_board = "Stuff I Won't Read", + theme = "Birthday Party", + thread_comment_required = "CONVERSE BEFORE CLOUD", + thread_file_required = "INSERT FILE FOR CLOUD", + slug = "CLOUD RESOURCE IDENTIFIER", + username = "User Identifcation String", + yes = "I Am Glad To Blindly Accept This", + no = "Not Exactly", + + -- Announcements + create_ann = "Open Mouth, Insert Foot", + modify_ann = "Damage Control", + delete_ann = "I never said that, you can't prove it.", + created_ann = "I believe %s is the lifeblood of this company.", + modified_ann = "What I meant to say was actually %s.", + deleted_ann = "What announcement? %s you say? Doesn't ring a bell.", + + -- Boards + create_board = "More Email!", + modify_board = "Different Email", + delete_board = "Less Email!", + created_board = "How do you feel about /%s/ - %s? How about agilefall?", + modified_board = "I hope we fixed the issue about /%s/ - %s because I was getting tired of covering for him.", + deleted_board = "I forgot I don't even like /%s/ - %s.", + + -- Pages + create_page = "Create new effigy to my greatness.", + modify_page = "Make this less bad.", + delete_page = "I ain't readin' this.", + created_page = "I heard about /%s/ - %s online, let me tell you how we can pivot this.", + modified_page = "I saw some stuff about /%s/ - %s so I fixed it.", + deleted_page = "Good news, I got rid of those books about: /%s/ - %s.", + + -- Reports + view_report = "I said *what*?", + delete_report = "Forget about that.", + deleted_report = "I changed my mind about %s and you can too.", + + -- Users + create_user = "Hire", + modify_user = "Rectify Person", + delete_user = "Get Rid Of", + created_user = "So %s is this bright young talent we've been hearing about.", + modified_user = "Whatever was wrong with %s we squared away.", + deleted_user = "I got rid of that %s character for you.", + + --==[[ Archive ]]==-- + + arc_display = "Displaying %{n_thread} expired %{p_thread} from the past %{n_day} %{p_day}", + arc_number = "In Britan they say 'pound'.", + arc_name = "Who?", + arc_excerpt = "TL;DR", + arc_replies = "Responses", + arc_view = "Investigate", + + --==[[ Ban ]]==-- + + ban_title = "Not allowed back!", + ban_reason = "I decided I don't like you anymore, because: ", + ban_expire = "I might forget about this on %{expire}.", + ban_ip = "And your stupid raffle ticket was: %{ip}.", + + --==[[ Catalog ]]==-- + + cat_stats = "R: %{replies} / F: %{files}", + + --==[[ Copyright ]]==-- + + copy_software = "You paid HOW MUCH for %{software}? And it's only version %{version}?! We should have used phpBB, it's way more mature.", + copy_download = "Negotiate a license from %{github}", + + --==[[ Forms ]]==-- + + form_ban = "Escort Off Property", + form_ban_display = "Not allowed in public", + form_ban_board = "Not allowed in my hosue", + form_ban_reason = "Why don't I like this person", + form_ban_time = "How long (in digital ages) to pretend I don't know this guy.", + form_clear = "Forget About It", + form_delete = "Hide Evidence", + form_draw = "Sketch Out", + form_lock = "END THIS", + form_override = "CLOUD IS LOOSE AND HUNGRY FOR DATA", + form_readme = "Frankly, I don't read the [%{rules}] and [%{faq}] so I won't ask you to, either. Hell, I don't even know what they say. And I wrote 'em!", + form_remix = "CLAIM AS YOUR OWN", + form_report = "I DON'T LIKE IT", + form_required = "MANDATORY IF YOU WANT TO KEEP WORKING HERE", + form_save = "Fwd to Offshore Account", + form_sticky = "IMMORTIALIZE SHAME", + form_submit = "HEMMORAGE BRILLIANCE", + form_submit_name = "EMPLOYEE ID NUMBER (PRE-ACQUISITION)", + form_submit_name_help = "Who are you again? A trip-what?", + form_submit_subject = "What Am I On About? Oh, Right.", + form_submit_subject_help = "What I've brought you all here today to discuss", + form_submit_options = "Stock Options", + form_submit_options_help = "sage: Don't let anyone know you're desperate for attention.", + form_submit_comment = "What you have to say.", + form_submit_comment_help = "Tell us about your great idea.", + form_submit_file = "DATUMS", + form_submit_file_help = "FOREFIT YOUR DATA TO THE CLOUD", + form_submit_draw = "MSPAINT", + form_submit_draw_help = "MSPAINT OR GIMPIFY", + form_submit_spoiler = "Paywalled Content", + form_submit_spoiler_help = "Increase viewer engagement 10%%", + form_submit_mod = "Enforcer of Marketability", + form_submit_mod_help = "Mark as landmine. Minesweeper humor. It means I think there's a bomb here.", + form_width = "Digital Horizon", + form_height = "Digital Embiggenment", + + --==[[ Posts ]]==-- + + post_link = "FORWARD THIS EMAIL", + post_lock = "EMAIL IS IN ARCHIVE MODE, STOP REPLYING TO THINGS I FORGOT ABOUT.", + post_hidden = "%{n_post} %{p_post} and %{n_file} %{p_file} omitted. %{click} to view.", + post_override = "CLOUD STORAGE ENABLED", + post_reply = "Reply to this email", + post_sticky = "reply-all'd, cc'd the company, 10/10", + post_save = "I refuse to cut down on the fat, on account of all the words I said being fluff meant to distract you.", + + --==[[ Plurals ]]==-- + + days = { + one = "digital age", + other = "digital aegis" + }, + files = { + one = "datum", + other = "datums" + }, + posts = { + one = "email", + other = "emailadoodles" + }, + threads = { + one = "email chain", + other = "clusterfuck" + }, +}} diff --git a/website/backend/app/src/locale/pl.lua b/website/backend/app/src/locale/pl.lua new file mode 100755 index 0000000..bdadaf3 --- /dev/null +++ b/website/backend/app/src/locale/pl.lua @@ -0,0 +1,240 @@ +return { pl = { + + --==[[ Navigation ]]==-- + + archive = "Archiwum", + bottom = "Przewiń na dół", + catalog = "Katalog", + index = "Spis treści", + refresh = "Odśwież", + ["return"] = "Powróć", + return_board = "Powróć do boardu", + return_index = "Powróć do spisu treści", + return_thread = "Powróć do wątku", + top = "Przewiń na górę", + + --==[[ Error Messages ]]==-- + + -- Controller error messages + err_ban_reason = "Brak powodu.", + err_board_used = "Nazwa boardu jest już w użyciu.", + err_not_admin = "Nie jesteś administratorem.", + err_orphaned = "Wątek %s został osierocony.", + err_slug_used = "Adres slug jest już w użyciu.", + err_user_used = "Nazwa użytkownika jest już w użyciu.", + + -- Model error messages + err_contribute = "Musisz załączyć treść posta lub załącznik.", + err_locked_thread = "Wątek %s jest zablokowany.", + err_no_files = "Dodawanie plików jest wyłączone na tym boardzie.", + + err_comment_post = "Na tym boardzie, by odpowiedzieć na wątek, musisz zamieścić komentarz.", + err_comment_thread = "Na tym boardzie, by stworzyć wątek, musisz zamieścić komentarz.", + + err_create_ann = "Nie można utworzyć ogłoszenia: %s.", + err_create_ban = "Nie można zbananować IP: %s.", + err_create_board = "Nie można utworzyć boarda: /%s/ - %s.", + err_create_page = "Nie można utworzyć strony: /%s/ - %s.", + err_create_post = "Nie można wysłać posta.", + err_create_report = "Nie można zgłosić posta %s.", + err_create_thread = "Nie można utworzyć wątku.", + + err_delete_board = "Nie można usunąć boarda: /%s/ - %s.", + err_delete_post = "Nie można usunąć posta %s.", + err_create_user = "Nie można utworzyć użytkownika: %s.", + + err_file_exists = "Ten plik już istnieje na tym boardzie.", + err_file_limit = "Wątek %s osiągnął już swój limit plików.", + err_file_post = "Na tym boardzie, by odpowiedzieć na wątek, musisz załączyć plik.", + err_file_thread = "Na tym boardzie, by stworzyć wątek, musisz załączyć plik.", + + err_invalid_board = "Nieznany board: /%s/.", + err_invalid_ext = "Nieznany typ pliku: %s.", + err_invalid_image = "Załączony plik nie jest prawidłowym obrazkiem.", + err_invalid_post = "Post %s nie jest poprawnym postem.", + err_invalid_user = "Niepoprawna nazwa użytkownika lub hasło.", + + --==[[ 404 ]]==-- + + ["404"] = "404 - Strona nie znaleziona", + + --==[[ Administration ]]==-- + + -- General + admin_panel = "Panel cwela", + administrator = "Cwel", + announcement = "Ogłoszenie", + archive_days = "Dni do archiwizowania wątków", + archive_pruned = "Archiwizuj wątki", + board = "Board", + board_group = "Grupa boarda", + board_title = "Nazwa boarda", + bump_limit = "Limit przyjebek", + content_md = "Opis (Markdown)", + default_name = "Domyślna nazwa", + draw_board = "Board do rysowania", + file = "Plik", + file_limit = "Limit plików w wątku", + global = "Globalnie", + index_boards = "Aktualne boardy", + janitor = "Woźny", + login = "Zaloguj", + logout = "Wyloguj", + moderator = "Moderator", + num_pages = "Aktywne strony", + num_threads = "Fredy na stronę", + password = "Hasło", + password_old = "Powtórz hasło", + password_retype = "Stare hasło", + post_comment_required = "Wymagany komentarz do posta", + post_file_required = "Wymagany plik do posta", + regen_thumb = "Odśwież miniaturki", + reply = "Odpowiedz", + rules = "Zasady", + name = "Nazwa", + subtext = "Podtekst", + success = "Sukces", + text_board = "Board tekstowy", + theme = "Motyw", + thread_comment_required = "Wymagany komentarz do freda", + thread_file_required = "Wymagany plik do freda", + slug = "Slug", + username = "Nazwa użytkownika", + yes = "Tak", + no = "Nie", + + -- Announcements + create_ann = "Utwórz ogłoszenie", + modify_ann = "Zmień ogłoszenie", + delete_ann = "Wyjeb ogłoszenie", + created_ann = "Utworzyłeś ogłoszenie %s.", + modified_ann = "Zmieniłeś ogłoszenie %s.", + deleted_ann = "Wyjebałeś ogłoszenie %s.", + + -- Boards + create_board = "Utwórz boarda", + modify_board = "Zmień boarda", + delete_board = "Wyjeb boarda", + created_board = "Utworzyłeś boarda /%s/ - %s.", + modified_board = "Zmieniłeś boarda /%s/ - %s.", + deleted_board = "Wyjebałeś boarda /%s/ - %s.", + + -- Pages + create_page = "Utwórz stronę", + modify_page = "Zmień stronę", + delete_page = "Wyjeb stronę", + created_page = "Utworzyłeś stronę /%s/ - %s.", + modified_page = "Zmieniłeś stronę /%s/ - %s.", + deleted_page = "Wyjebałeś stronę /%s/ - %s.", + + -- Reports + view_report = "Przejrzyj zgłoszenie", + delete_report = "Usuń zgłoszenie", + deleted_report = "Wyjebałeś zgłoszenie %s.", + + -- Users + create_user = "Utwórz użytkownika", + modify_user = "Zmień użytkownika", + delete_user = "Wyjeb użytkownika", + created_user = "Utworzyłeś użytkownika %s.", + modified_user = "Zmieniłeś użytkownika %s.", + deleted_user = "Wyjebałeś użytkownika %s.", + + --==[[ Archive ]]==-- + + arc_display = "Wyświetlanie %{n_thread} %{p_thread} (zarchiwizowane) z %{n_day} %{p_day}", + arc_number = "nr ", + arc_name = "Nazwa", + arc_excerpt = "Kawał freda", + arc_replies = "Odpowiedzi", + arc_view = "Wyślij", + + --==[[ Ban ]]==-- + + ban_title = "Zbanowany!", + ban_reason = "Zostałeś zbananowany z powodu o takiego:", + ban_expire = "Twój banan usunie się %{expire}.", + ban_ip = "Według naszych serwerów NASA, twoje IP to %{ip}.", + + --==[[ Catalog ]]==-- + + cat_stats = "Odp.: %{replies} / plików: %{files}", + + --==[[ Copyright ]]==-- + + copy_software = "Fredy napędzane przez %{software} %{version}", + copy_download = "Pobierz z %{github}", + + --==[[ Forms ]]==-- + + form_ban = "Zbanuj użytkownika", + form_ban_display = "Wyświetl bana", + form_ban_board = "Ban lokalny", + form_ban_reason = "Powód bana", + form_ban_time = "Długość bana (w minutach)", + form_clear = "Wyczyść", + form_delete = "Usuń posta", + form_draw = "Rysuj", + form_lock = "Zablokuj wątek", + form_override = "Nielimitowane pliki", + form_readme = "Przeczytaj [%{rules}] oraz [%{faq}] zanim zapostujesz.", + form_remix = "Przerób obrazek", + form_report = "Zgłoś posta", + form_required = "pole wymagane", + form_save = "Zapisz wątek", + form_sticky = "Przyklej freda", + form_submit = "Wyślij posta", + form_submit_name = "Pseudonim", + form_submit_name_help = "Nadaj sobie nazwę lub tripkod (opcjonalne)", + form_submit_subject = "Temat", + form_submit_subject_help = "Temat dyskusji (opcjonalne)", + form_submit_options = "Opcje", + form_submit_options_help = "sage: zapostuj bez podbijania freda (opcjonalne)", + form_submit_comment = "Komentarz", + form_submit_comment_help = "Dodaj coś do dyskusji", + form_submit_file = "Plik", + form_submit_file_help = "Załącz plik", + form_submit_draw = "Rysuj", + form_submit_draw_help = "Narysuj lub przerób obrazek", + form_submit_spoiler = "Spoiler", + form_submit_spoiler_help = "Podmień miniaturkę na spoiler", + form_submit_mod = "Moderator", + form_submit_mod_help = "Oznacz tego freda", + form_width = "Szerokość", + form_height = "Wysokość", + + --==[[ Posts ]]==-- + + post_link = "Link do tego posta", + post_lock = "Wątek jest zablokowany", + post_hidden = "%{n_post} %{p_post} i %{n_file} %{p_file} zostały pominięte. %{click} aby wyświetlić.", + post_override = "Wątek akceptuje nielimitowaną ilość plików", + post_reply = "Odpowiedz na ten post", + post_sticky = "Fred jest przyklejony", + post_save = "Wątek nie będzie archiwizowany", + + --==[[ Plurals ]]==-- + + days = { + one = "dzień", + other = "dni" + }, + files = { + one = "plik", + few = "pliki", + many = "plików", + other = "pliki" + }, + posts = { + one = "post", + few = "posty", + many = "postów", + other = "posty" + }, + threads = { + one = "wątek", + few = "wątki", + many = "wątków", + other = "wątki" + }, +}} diff --git a/website/backend/app/src/models.lua b/website/backend/app/src/models.lua new file mode 100755 index 0000000..0e12ba4 --- /dev/null +++ b/website/backend/app/src/models.lua @@ -0,0 +1 @@ +return require("lapis.util").autoload "models" diff --git a/website/backend/app/src/models/announcements.lua b/website/backend/app/src/models/announcements.lua new file mode 100755 index 0000000..35c9fbf --- /dev/null +++ b/website/backend/app/src/models/announcements.lua @@ -0,0 +1,96 @@ +local Model = require("lapis.db.model").Model +local Announcements = Model:extend("announcements", { + relations = { + { "board", belongs_to="Boards" } + } +}) + +Announcements.valid_record = { + { "board_id", is_integer=true }, + { "text", exists=true } +} + +--- Create an announcement +-- @tparam table params Announcement parameters +-- @treturn boolean success +-- @treturn string error +function Announcements:new(params) + local announcement = self:create(params) + return announcement and announcement or nil, { "err_create_ann", { params.text } } +end + +--- Modify an announcement +-- @tparam table params Announcement parameters +-- @treturn boolean success +-- @treturn string error +function Announcements:modify(params) + local announcement = self:get(params.id) + if not announcement then + return nil, { "err_create_ann", { params.text } } -- FIXME: wrong error + end + + local success, err = announcement:update(params) + return success and announcement or nil, "FIXME: " .. tostring(err) +end + +--- Delete an announcement +-- @tparam number id Announcement ID +-- @treturn boolean success +-- @treturn string error +function Announcements:delete(id) + local announcement = self:get(id) + if not announcement then + return nil, "FIXME" + end + + local success = announcement:delete() + return success and announcement or nil, "FIXME" +end + +--- Get all announcements +-- @treturn boolean success +-- @treturn string error +function Announcements:get_all() + local announcements = self:select("order by board_id asc") + return announcements +end + +--- Get announcements +-- @treturn boolean success +-- @treturn string error +function Announcements:get_global() + local announcements = self:select("where board_id=0") + return announcements +end + +--- Get board announcements +-- @tparam number board_id Board ID +-- @treturn boolean success +-- @treturn string error +function Announcements:get_board(board_id) + local announcements = self:select("where board_id=?", board_id) + return announcements +end + +--- Get announcement +-- @tparam number id Announcement ID +-- @treturn boolean success +-- @treturn string error +function Announcements:get(id) + local announcement = self:find(id) + return announcement and announcement or nil, "FIXME" +end + +function Announcements.format_to_db(_, params) + if not params.board_id then + params.board_id = 0 + end +end + +function Announcements.format_from_db(_, params) + if params.board_id == 0 then + params.board_id = nil + end +end + +return Announcements diff --git a/website/backend/app/src/models/bans.lua b/website/backend/app/src/models/bans.lua new file mode 100755 index 0000000..66e6c8f --- /dev/null +++ b/website/backend/app/src/models/bans.lua @@ -0,0 +1,151 @@ +local Model = require("lapis.db.model").Model +local Bans = Model:extend("bans", { + relations = { + { "board", belongs_to="Boards" }, + } +}) + +Bans.valid_record = { + { "board_id", is_integer=true }, + { "ip", max_length=255, exists=true }, + { "time", exists=true } +} + +--- Create a ban +-- @tparam table params Ban parameters +-- @treturn boolean success +-- @treturn string err +function Bans:new(params) + local ban = self:create(params) + return ban and ban or nil, { "err_create_ban", { params.ip } } +end + +--- Modify a ban +-- @tparam table params Board parameters +-- @treturn boolean success +-- @treturn string error +function Bans:modify(params) + local ban = self:get(params.id) + if not ban then + return nil, { "err_create_board", { params.name, params.title } } -- FIXME: wrong error message + end + + local success, err = ban:update(params) + return success and ban or nil, "FIXME: " .. tostring(err) +end + +--- Delete a ban +-- @tparam number id Ban's ID +-- @treturn boolean success +-- @treturn string err +function Bans:delete(id) + local ban = self:get(id) + if not ban then + return nil, "FIXME" + end + + local success = ban:delete() + return success and ban or nil, "FIXME" +end + +--- Get all bans +-- @treturn table users List of bans +function Bans:get_all() + local bans = self:select("order by board_id asc, time + duration desc, ip asc") + + for i=#bans, 1, -1 do + local ban = bans[i] + if not self:validate(ban) then + table.remove(bans, i) + end + end + + return bans +end + +--- Get board bans +-- @treturn table users List of bans +function Bans:get_board(board_id) + local bans = self:select("where board_id=? order by board_id asc, time + duration desc, ip asc", board_id) + + for i=#bans, 1, -1 do + local ban = bans[i] + if not self:validate(ban) then + table.remove(bans, i) + end + end + + return bans +end + +--- Get ban data +-- @tparam number id Ban's ID +-- @treturn table ban +function Bans:get(id) + local ban = self:find(id) + if not ban then + return nil, "FIXME: ALART!" + end + + local valid, err = self:validate(ban) + if not valid then + return nil, err + end + + return ban +end + +--- Get bans for specific ip +-- @tparam string ip IP address +-- @treturn table ban +function Bans:get_ip(ip) + local bans = self:select("where ip=?", ip) + + for i=#bans, 1, -1 do + local ban = bans[i] + if not self:validate(ban) then + table.remove(bans, i) + end + end + + return bans +end + +--- Validate ban +-- @tparam table ban Ban data +-- @treturn boolean valid +function Bans:validate(ban) + local time = os.time() + local finish = ban.time + ban.duration + + if time >= finish then + self:delete(ban) + return nil, "FIXME: ban has exired" + end + + return true +end + +--- Format ban paramaters for DB insertion +-- @tparam table params Ban parameters +function Bans.format_to_db(_, params) + -- Convert duration from days to seconds + params.duration = (tonumber(params.duration) or 0) * 86400 + + if not params.board_id then + params.board_id = 0 + end +end + +--- Format ban parameters for User consumption +-- @tparam table params Ban parameters +function Bans.format_from_db(_, params) + -- Convert duration from seconds to days + params.duration = tonumber(params.duration) / 86400 + + if params.board_id == 0 then + params.board_id = nil + end +end + +return Bans diff --git a/website/backend/app/src/models/boards.lua b/website/backend/app/src/models/boards.lua new file mode 100755 index 0000000..46ee3ce --- /dev/null +++ b/website/backend/app/src/models/boards.lua @@ -0,0 +1,226 @@ +local db = require "lapis.db" +local giflib = require "giflib" +local lfs = require "lfs" +local magick = require "magick" +local Model = require("lapis.db.model").Model +local Boards = Model:extend("boards", { + relations = { + { "announcements", has_many="Announcements" }, + { "bans", has_many="Bans" }, + { "posts", has_many="Posts" }, + { "reports", has_many="Reports" }, + { "threads", has_many="Threads", where={ archive=false }, order="sticky desc, last_active desc" }, + { "archived", has_many="Threads", where={ archive=true }, order="last_active desc" }, + } +}) + +Boards.valid_record = { + { "name", max_length=255, exists=true }, + { "title", max_length=255, exists=true }, + { "subtext", max_length=255 }, + { "ban_message", max_length=255 }, + { "anon_name", max_length=255 }, + { "theme", max_length=255 }, + { "pages", exists=true }, + { "threads_per_page", exists=true }, + { "thread_file_limit", exists=true }, + { "post_limit", exists=true }, + { "archive_time", exists=true }, + { "group", exists=true } +} + +--- Create a board +-- @tparam table params Board parameters +-- @treturn boolean success +-- @treturn string error +function Boards:new(params) + local board = self:create(params) + if not board then + return false, { "err_create_board", { params.name, params.title } } + end + + lfs.mkdir(string.format("./static/%s/", board.name)) + return board +end + +--- Modify a board +-- @tparam table params Board parameters +-- @tparam old_name Board's current short name +-- @treturn boolean success +-- @treturn string error +function Boards:modify(params, old_name) + local board = self:get(old_name) + if not board then + return false, { "err_create_board", { params.name, params.title } } -- FIXME: wrong error message + end + + local success, err = board:update(params) + if not success then + return false, "FIXME: " .. tostring(err) + end + + if board.name ~= old_name then + local old = string.format("./static/%s/", old_name) + local new = string.format("./static/%s/", board.name) + os.rename(old, new) + end + + return board +end + +--- Delete a board +-- @tparam string name Board's short name +-- @treturn boolean success +-- @treturn string error +function Boards:delete(name) + local board = self:get(name) + if not board then + return false, { "err_create_board", { name, name } } -- FIXME: wrong error message + end + + local announcements = board:get_announcements() + local bans = board:get_bans() + local posts = board:get_posts() + local reports = board:get_reports() + local threads = board:get_threads() + local dir = string.format("./static/%s/", board.name) + + -- Clear data + for _, announcement in ipairs(announcements) do announcement:delete() end + for _, ban in ipairs(bans) do ban:delete() end + for _, post in ipairs(posts) do post:delete() end + for _, report in ipairs(reports) do report:delete() end + for _, thread in ipairs(threads) do thread:delete() end + + -- Clear filesystem + if lfs.attributes(dir, "mode") == "directory" then + -- Delete files + for file in lfs.dir(dir) do + os.remove(dir .. file) + end + + -- Delete directory + lfs.rmdir(dir) + end + + -- Clear board + local success = board:delete() + return success and board or false, { "err_delete_board", { board.name, board.title } } +end + +--- Get all boards +-- @treturn table boards +function Boards:get_all() + local boards = self:select("order by boards.group asc, name asc") + return boards and boards or false, "FIXME: ALART!" +end + +--- Get board data +-- @tparam string name Board's short name +-- @treturn table board +function Boards:get(name) + local board = self:find { name=name } + return board and board or false, "FIXME: ALART!" +end + +--- Format board paramaters for DB insertion +-- @tparam table params Board parameters +function Boards.format_to_db(_, params) + -- Convert archive_time from days to seconds + params.archive_time = (tonumber(params.archive_time) or 0) * 86400 +end + +--- Format board parameters for User consumption +-- @tparam table params Board parameters +function Boards.format_from_db(_, params) + -- Convert archive_time from seconds to days + params.archive_time = tonumber(params.archive_time) / 86400 +end + +--- Regenerate thumbnails for all posts +-- @treturn none +function Boards.regen_thumbs(_) + local sql = [[ + select + boards.name as board, + posts.thread_id, + posts.file_path, + posts.file_width, + posts.file_height, + posts.file_type + from posts + left join boards on + board_id = boards.id + where + file_path is not null and + file_width is not null and + file_height is not null and + file_type = 'image' and + file_spoiler = false + order by + board_id asc, + thread_id asc, + post_id asc + ]] + local dir + local board + local thread = 0 + local results = db.query(sql) + + for _, result in ipairs(results) do + -- Change board, reset thread counter and image directory + if result.board ~= board then + board = result.board + thread = 0 + dir = string.format("./static/%s/", board) + end + + -- Filesystem paths + local name, ext = result.file_path:match("^(.+)(%..+)$") + ext = string.lower(ext) + + local full_path = dir .. result.file_path + local thumb_path = dir .. "s" .. result.file_path + + -- Generate a thumbnail + if ext == ".webm" then + thumb_path = dir .. "s" .. name .. ".png" + + -- Create screenshot of first frame + os.execute(string.format("ffmpeg -i %s -ss 00:00:01 -vframes 1 %s -y", full_path, thumb_path)) + end + + -- Save thumbnail + local w, h + if result.thread_id > thread then + thread = result.thread_id + w = result.file_width < 250 and result.file_width or 250 + h = result.file_height < 250 and result.file_height or 250 + else + w = result.file_width < 125 and result.file_width or 125 + h = result.file_height < 125 and result.file_height or 125 + end + + -- Grab first frame from video + if ext == ".webm" then + magick.thumb(thumb_path, string.format("%sx%s", w, h), thumb_path) + elseif ext == ".svg" then + thumb_path = dir .. "s" .. name .. ".png" + os.execute(string.format("convert -background none -resize %dx%d %s %s", w, h, full_path, thumb_path)) + elseif ext == ".gif" then + -- Grab first frame of a gif instead of the last + local gif, err = giflib.load_gif(full_path) + + if err then + magick.thumb(full_path, string.format("%sx%s", w, h), thumb_path) + else + gif:write_first_frame(thumb_path) + magick.thumb(thumb_path, string.format("%sx%s", w, h), thumb_path) + end + else + magick.thumb(full_path, string.format("%sx%s", w, h), thumb_path) + end + end +end + +return Boards diff --git a/website/backend/app/src/models/pages.lua b/website/backend/app/src/models/pages.lua new file mode 100755 index 0000000..82ce112 --- /dev/null +++ b/website/backend/app/src/models/pages.lua @@ -0,0 +1,90 @@ +local Model = require("lapis.db.model").Model +local Pages = Model:extend("pages") + +Pages.valid_record = { + { "slug", exists=true, type="String" }, + { "title", exists=true, type="String" }, + { "content", exists=true, type="String" } +} + +--- Create a new page +-- @tparam table page Page data +-- @treturn boolean success +-- @treturn string error +function Pages:new(params) + local unique, err = self:is_unique(params.slug, params.title) + if not unique then + return nil, err + end + + local page = self:create(params) + return page and page or nil, { "err_create_page", { page.slug, page.title } } +end + +--- Modify a page +-- @tparam table page Page data +-- @treturn boolean success +-- @treturn string error +function Pages:modify(params, slug) + local page = self:get(slug) + if not page then + return nil, "FIXME" + end + + -- Check to see if the page we are modifying is the one that fails validation. + -- If it is, that's fine since we're updating the unique values with themselves. + -- If #pages > 1 then this will always fail since either the new slug or new + -- title is going to belong to some other page. + do + local unique, err, pages = self:is_unique(params.slug, params.title) + if not unique then + for _, p in ipairs(pages) do + if page.id ~= p.id then + return nil, err + end + end + end + end + + local success, err = page:update(params) + return success and page or nil, "FIXME: " .. tostring(err) +end + +--- Delete page +-- @tparam table page Page data +-- @treturn boolean success +-- @treturn string error +function Pages:delete(slug) + local page = self:get(slug) + if not page then + return nil, "FIXME" + end + + local success = page:delete() + return success and page or nil, "FIXME" +end + +--- Get all pages +-- @treturn table pages List of pages +function Pages:get_all() + return self:select("order by slug asc") +end + +--- Get page +-- @tparam string slug Page slug +-- @treturn table page +function Pages:get(slug) + local page = self:find { slug=slug:lower() } + return page and page or nil, "FIXME" +end + +function Pages:is_unique(slug, title) + local pages = self:select("where slug=? or lower(title)=?", slug, title:lower()) + return #pages == 0 and true or nil, "FIXME", pages +end + +function Pages.format_to_db(_, params) + params.slug = params.slug:lower() +end + +return Pages diff --git a/website/backend/app/src/models/posts.lua b/website/backend/app/src/models/posts.lua new file mode 100755 index 0000000..e53d792 --- /dev/null +++ b/website/backend/app/src/models/posts.lua @@ -0,0 +1,387 @@ +local encoding = require "lapis.util.encoding" +local Model = require("lapis.db.model").Model +local giflib = require "giflib" +local magick = require "magick" +local md5 = require "md5" +local filetypes = require "utils.file_whitelist" +local generate = require "utils.generate" +local Posts = Model:extend("posts", { + relations = { + { "board", belongs_to="Boards" }, + { "thread", belongs_to="Threads" }, + } +}) +local sf = string.format + +local function get_duration(path) + local cmd = sf("ffprobe -i %s -show_entries format=duration -v quiet -of csv=\"p=0\" -sexagesimal", path) + local f = io.popen(cmd, "r") + local s = f:read("*a") + f:close() + + local hr, mn, sc = string.match(s, "(%d+):(%d+):(%d+).%d+") + local d = mn..":"..sc + + if hr ~= "0" then + d = hr..":"..d + end + + return d +end + +--- Prepare post for insertion +-- @tparam table params Input from the user +-- @tparam table session User session +-- @tparam table board Board data +-- @tparam table thread Thread data +-- @tparam number files Number of files in thread +-- @treturn boolean success +-- @treturn string error +function Posts:prepare_post(params, session, board, thread, files) + -- FIXME: this whole function should be web-side, not api-side + local time = os.time() + + -- Prepare session stuff + session.password = session.password or generate.password(time) + + -- Save names on individual boards + -- TODO: Put this code elsewhere + if params.name then + session.names[board.name] = params.name + end + + -- Files take presidence over drawings, but if there is no file, fill in + -- the file fields with draw data. This should avoid ugly code branching + -- later on in the file. + -- TODO: send file data through api! + params.file = params.file or { + filename = "", + content = "" + } + + if #params.file.content == 0 and params.draw then + local pattern = ".-,(.+)" + params.draw = params.draw:match(pattern) + params.file.filename = "tegaki.png" + params.file.content = encoding.decode_base64(params.draw) + end + + -- Check board flags + if thread then + if thread.lock and not session.admin and not session.mod then + return false, { "err_locked_thread", { thread.post_id } } + end + + if board.post_comment and not params.comment then + return false, { "err_comment_post" } + end + + if board.post_file and #params.file.content == 0 then + return false, { "err_file_post" } + end + else + if board.thread_comment and not params.comment then + return false, { "err_comment_thread" } + end + + if board.thread_file and #params.file.content == 0 then + return false, { "err_file_thread" } + end + end + + -- Parse name + if params.name then + params.name, params.trip = generate.tripcode(params.name) + end + + -- Set file + if #params.file.content > 0 then + + -- Reject files in text-only boards + if board.text_only then + return false, { "err_no_files" } + end + + -- Thread limit is already met. + if thread then + if files >= board.thread_file_limit and not thread.size_override then + return false, { "err_file_limit", { thread.post_id } } + end + end + + local name = sf("%s%s", time, generate.random()) + local ext = params.file.filename:match("^.+(%..+)$") + ext = string.lower(ext) + + -- Figure out how to deal with the file + if filetypes.image[ext] and board.filetype_image then + params.file_type = "image" + + if ext ~= ".webm" then + -- Check if valid image + local image = magick.load_image_from_blob(params.file.content) + + if not image then + return false, { "err_invalid_image" } + end + + params.file_width = image:get_width() + params.file_height = image:get_height() + end + elseif filetypes.audio[ext] and board.filetype_audio then + params.file_type = "audio" + else + return false, { "err_invalid_ext", { ext } } + end + + params.file_name = params.file.filename + params.file_path = name .. ext + params.file_md5 = md5.sumhexa(params.file.content) + params.file_size = #params.file.content + + if params.file_spoiler then + params.file_spoiler = true + else + params.file_spoiler = false + end + + -- Check if file already exists + local file = self:find_file(board.id, params.file_md5) + if file then + return false, { "err_file_exists" } + end + else + params.file_spoiler = false + end + + -- Check contributions + if not params.comment and not params.file_name then + return false, { "err_contribute" } + end + + return true +end + +--- Create a new post +-- @tparam table params Post parameters +-- @tparam table board Board data +-- @tparam boolean op OP flag +-- @treturn boolean success +-- @treturn string error +function Posts:new(params, board, op) + + -- Create post + local post = self:create(params) + if not post then + return false, { "err_create_post" } + end + + -- Save file + if post.file_path then + local dir = sf("./static/%s/", board.name) + local name, ext = post.file_path:match("^(.+)%.(.+)$") + ext = string.lower(ext) + + -- Filesystem paths + local full_path = dir .. post.file_path + local thumb_path = dir .. "s" .. post.file_path + + -- Save file + local file = io.open(full_path, "w") + file:write(params.file_content) + file:close() + + -- Audio file + if post.file_type == "audio" then + post.file_duration = get_duration(full_path) + post:update("file_duration") + return post + end + + -- Image file + if post.file_type == "image" and not post.file_spoiler then + + -- Save thumbnail + local w, h + if op then + w = post.file_width < 250 and post.file_width or 250 + h = post.file_height < 250 and post.file_height or 250 + else + w = post.file_width < 125 and post.file_width or 125 + h = post.file_height < 125 and post.file_height or 125 + end + + -- Generate a thumbnail + if ext == "webm" then + thumb_path = dir .. "s" .. name .. ".png" + + -- Create screenshot of first frame + os.execute(sf("ffmpeg -i %s -ss 00:00:01 -vframes 1 %s -y", full_path, thumb_path)) + + -- Update post info + local image = magick.load_image(thumb_path) + post.file_width = image:get_width() + post.file_height = image:get_height() + post.file_duration = get_duration(full_path) + post:update("file_width", "file_height", "file_duration") + + -- Resize thumbnail + magick.thumb(thumb_path, sf("%sx%s", w, h), thumb_path) + + elseif ext == "svg" then + thumb_path = dir .. "s" .. name .. ".png" + os.execute(sf("convert -background none -resize %dx%d %s %s", w, h, full_path, thumb_path)) + + elseif ext == "gif" then + local gif, err = giflib.load_gif(full_path) + + if err then + -- Not animated I presume? TODO: check what err represents + magick.thumb(full_path, sf("%sx%s", w, h), thumb_path) + else + -- Grab first frame of a gif instead of the last + gif:write_first_frame(thumb_path) + + -- Update post info + local width, height = gif:dimensions() + post.file_width = width + post.file_height = height + post:update("file_width", "file_height") + + -- Resize thumbnail + magick.thumb(thumb_path, sf("%sx%s", w, h), thumb_path) + end + else + magick.thumb(full_path, sf("%sx%s", w, h), thumb_path) + end + end + end + + -- Update board + board:update("total_posts") + + return post +end + +--- Delete post data +-- @tparam number id Post ID +-- @treturn boolean success +-- @treturn string error +function Posts:delete(id) + + -- Get post + local post, err = self:get_post_by_id(id) + if not post then + return false, err + end + + -- Delete post + local success, err = post:delete() + if not success then + return false, err--{ "err_delete_post", { post.post_id } } + end + + -- Delete files + if post.file_path then + local board = post:get_board() + local dir = sf("./static/%s/", board.name) + local name, ext = post.file_path:match("^(.+)%.(.+)$") + ext = string.lower(ext) + os.remove(dir .. post.file_path) + + -- Change thumbnail path to png + if ext == "webm" or ext == "svg" then + post.file_path = name .. ".png" + end + + os.remove(dir .. "s" .. post.file_path) + end + + return post +end + +--- Get op and last 5 posts of a thread to display on board index +-- @tparam number thread_id Thread ID +-- @treturn table posts +function Posts:get_index_posts(thread_id) + local sql = "where thread_id=? order by post_id desc limit 5" + local posts = self:select(sql, thread_id) + + if self:count_posts(thread_id) > 5 then + local thread = posts[1]:get_thread() + local op = thread:get_op() + table.insert(posts, op) + end + + return posts +end + +--- Get post data +-- @tparam number board_id Board ID +-- @tparam number post_id Local Post ID +-- @treturn table post +function Posts:get(board_id, post_id) + local post = self:find { + board_id = board_id, + post_id = post_id + } + return post and post or false, "FIXME" +end + +--- Get post data +-- @tparam number id Post ID +-- @treturn table post +function Posts:get_post_by_id(id) + local post = self:find(id) + return post and post or false, "FIXME" +end + +--- Find file in active posts +-- @tparam number board Board ID +-- @tparam string file_md5 Unique hash of file +-- @treturn boolean success +-- @treturn string error +function Posts:find_file(board_id, file_md5) + local sql = "where board_id=? and file_md5=? limit 1" + return unpack(self:select(sql, board_id, file_md5)) +end + + +--- Count hidden posts in a thread +-- @tparam number thread_id Thread ID +-- @treturn table hidden +function Posts:count_hidden_posts(thread_id) + local posts = self:get_index_posts(thread_id) + local num_posts = self:count_posts(thread_id) + local num_files = self:count_files(thread_id) + + for _, post in ipairs(posts) do + -- Reduce number of posts hidden + num_posts = num_posts - 1 + + -- Reduce number of files hidden + if post.file_name then + num_files = num_files - 1 + end + end + + return { posts=num_posts, files=num_files } +end + +--- Count posts in a thread +-- @tparam number thread_id Thread ID +-- @treturn number posts +function Posts:count_posts(thread_id) + local sql = "thread_id=?" + return self:count(sql, thread_id) +end + +--- Count posts with images in a thread +-- @tparam number thread_id Thread ID +-- @treturn number files +function Posts:count_files(thread_id) + local sql = "thread_id=? and file_name is not null" + return self:count(sql, thread_id) +end + +return Posts diff --git a/website/backend/app/src/models/reports.lua b/website/backend/app/src/models/reports.lua new file mode 100755 index 0000000..bbe1f9c --- /dev/null +++ b/website/backend/app/src/models/reports.lua @@ -0,0 +1,76 @@ +local trim = require("lapis.util").trim_filter +local Model = require("lapis.db.model").Model +local Reports = Model:extend("reports") + +--- Create a new report +-- @tparam table report Report data +-- @treturn boolean success +-- @treturn string error +function Reports:create_report(report) + -- Trim white space + trim(report, { + "board_id", "thread_id", "post_id", + "timestamp", "num_reports" + }, nil) + + local r = self:create { + board_id = report.board_id, + thread_id = report.thread_id, + post_id = report.post_id, + timestamp = report.timestamp, + num_reports = report.num_reports + } + + if r then + return r + end + + return false, { "err_create_report" } +end + +--- Modify a report +-- @tparam table report Report data +-- @treturn boolean success +-- @treturn string error +function Reports:modify_report(report) + local columns = {} + for col in pairs(report) do + table.insert(columns, col) + end + + return report:update(unpack(columns)) +end + +--- Delete report +-- @tparam table report Report data +-- @treturn boolean success +-- @treturn string error +function Reports:delete_report(report) + return report:delete() +end + +--- Get all reports +-- @treturn table reports List of reports +function Reports:get_reports() + return self:select("order by timestamp asc") +end + +--- Get report +-- @tparam string board_id Board ID +-- @tparam string post_id Post ID +-- @treturn table report +function Reports:get_report(board_id, post_id) + return unpack(self:select( + "where board_id=? and post_id=? limit 1", + board_id, post_id + )) +end + +--- Get report +-- @tparam string id Report ID +-- @treturn table report +function Reports:get_report_by_id(id) + return unpack(self:select("where id=? limit 1", id)) +end + +return Reports diff --git a/website/backend/app/src/models/threads.lua b/website/backend/app/src/models/threads.lua new file mode 100755 index 0000000..d33d00a --- /dev/null +++ b/website/backend/app/src/models/threads.lua @@ -0,0 +1,106 @@ +local Model = require("lapis.db.model").Model +local Threads = Model:extend("threads", { + relations = { + { "board", belongs_to="Boards" }, + { "posts", has_many="Posts" }, + { "op", has_one="Posts", order="post_id asc" }, + } +}) + +Threads.valid_record = { + { "board_id", exists=true } +} + +--- Create thread +-- @tparam table params Thread parameters +-- @treturn boolean success +-- @treturn string error +function Threads:new(params) + local thread = self:create(params) + return thread and thread or false, { "err_create_thread" } +end + +--- Modify a thread +-- @tparam table params Thread parameters +-- @treturn boolean success +-- @treturn string error +function Threads:modify(params) + local thread = self:get(params.id) + if not thread then + return false, { "err_create_board" } -- FIXME: wrong error message + end + + local success, err = thread:update(params) + return success and thread or false, "FIXME: " .. tostring(err) +end + +--- Delete entire thread +-- @tparam number id Thread ID +-- @treturn boolean success +-- @treturn string error +function Threads:delete(id) + -- FIXME: API needs to create a user object for better auth checking + local thread, err = self:get(id) + if not thread then + return false, err + end + + local op = thread:get_op() + local success = thread:delete() + return success and thread or false, { "err_delete_thread", { op.post_id } } +end + +--- Get thread data +-- @tparam number id Thread ID +-- @treturn table thread +function Threads:get(id) + local thread = self:find(id) + return thread and thread or false, "FIXME" +end + +--- Get archived threads +-- @tparam number board_id Board ID +-- @treturn table threads +function Threads:get_archived(board_id) + local sql = "where board_id=? and archive=true order by last_active desc" + return self:select(sql, board_id) +end + +--- Bump threads to archive +-- @tparam number board_id Board ID +-- @tparam number max_threads Maximum number of threads on this board +-- @treturn boolean success +-- @treturn string error +function Threads:archive_threads(board_id, max_threads) + local threads = self:get_threads(board_id) + + if #threads > max_threads then + for i=max_threads+1, #threads do + local _, err = self:archive_thread(threads[i]) + + if err then + return false, err + end + end + end +end + +--- Archive a thread +-- @tparam table thread Thread data +-- @treturn boolean success +-- @treturn string error +function Threads:archive_thread(thread) + thread.sticky = false + thread.lock = true + thread.archive = true + thread.last_active = os.time() + return thread:update("sticky", "lock", "archive", "last_active") +end + +--- Find threads with no posts +-- @treturn table threads +function Threads:find_orphans() + return self:select("where id not in (select distinct thread_id from posts)") +end + +return Threads diff --git a/website/backend/app/src/models/users.lua b/website/backend/app/src/models/users.lua new file mode 100755 index 0000000..469d6f2 --- /dev/null +++ b/website/backend/app/src/models/users.lua @@ -0,0 +1,181 @@ +local bcrypt = require "bcrypt" +local uuid = require "resty.jit-uuid" +local config = require("lapis.config").get() +local Model = require("lapis.db.model").Model +local Users = Model:extend("users") +local token = config.secret + +Users.role = { + [-1] = "INVALID", + [1] = "USER", + [6] = "JANITOR", + [7] = "MOD", + [8] = "ADMIN", + [9] = "OWNER", + + INVALID = -1, + USER = 1, + JANITOR = 6, + MOD = 7, + ADMIN = 8, + OWNER = 9 +} + +Users.valid_record = { + { "username", exists=true }, + { "role", exists=true, is_integer=true } +} + +Users.default_key = "00000000-0000-0000-0000-000000000000" + +--- Create a new user +-- @tparam table user User data +-- @treturn boolean success +-- @treturn string error +function Users:new(params, raw_password) + + -- Check if username is unique + do + local unique, err = self:is_unique(params.username) + if not unique then return nil, err end + end + + -- Verify password + do + local valid, err = self:validate_password(params.password, params.confirm, raw_password) + if not valid then return nil, err end + + params.confirm = nil + params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12) + end + + -- Generate unique API key + do + local api_key, err = self:generate_api_key() + if not api_key then return nil, err end + + params.api_key = api_key + end + + local user = self:create(params) + return user and user or nil, { "err_create_user", { params.username } } +end + +--- Modify a user +-- @tparam table user User data +-- @treturn boolean success +-- @treturn string error +function Users:modify(params, raw_username, raw_password) + local user = self:get(raw_username) + if not user then return nil, "FIXME" end + + -- Check if username is unique + do + local unique, err, u = self:is_unique(params.username) + if not unique and user.id ~= u.id then return nil, err end + end + + -- Verify password + if params.password then + local valid, err = self:validate_password(params.password, params.confirm, raw_password) + if not valid then return nil, err end + + params.confirm = nil + params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12) + end + + -- Generate unique API key + if params.api_key then + local api_key, err = self:generate_api_key() + if not api_key then return nil, err end + + params.api_key = api_key + end + + local success, err = user:update(params) + return success and user or nil, "FIXME: " .. tostring(err) +end + +--- Delete user +-- @tparam table user User data +-- @treturn boolean success +-- @treturn string error +function Users:delete(username) + local user = self:get(username) + if not user then + return nil, "FIXME" + end + + local success = user:delete() + return success and user or nil, "FIXME" +end + +--- Verify user +-- @tparam table params User data +-- @treturn boolean success +-- @treturn string error +function Users:login(params) + local user = self:get(params.username) + if not user then return nil, { "err_invalid_user" } end + + local password = user.username .. params.password .. token + local verified = bcrypt.verify(password, user.password) + + return verified and user or nil, { "err_invalid_user" } +end + +--- Get all users +-- @treturn table users List of users +function Users:get_all() + local users = self:select("order by username asc") + return users +end + +--- Get user +-- @tparam string username Username +-- @treturn table user +function Users:get(username) + local users = self:select("where lower(username)=? limit 1", username:lower()) + return #users == 1 and users[1] or nil, "FIXME" +end + +function Users:get_api(params) + local user = self:find(params) + return user and user or nil, "FIXME" +end + +function Users:format_to_db(params) + if not params.role then + params.role = self.role.INVALID + end +end + +function Users.format_from_db(_, params) + params.password = nil + params.api_key = nil +end + +function Users:is_unique(username) + local user = self:get(username) + return not user and true or nil, "FIXME", user +end + +function Users.validate_password(_, password, confirm, old_password) + if password ~= confirm or password ~= old_password then + return nil, "FIXME" + end + + return true +end + +function Users:generate_api_key() + for _ = 1, 10 do + local api_key = uuid() + local user = self:find { api_key=api_key } + if not user then return api_key end + end + + return nil, "FIXME" +end + +return Users diff --git a/website/backend/app/src/sass/posts.scss b/website/backend/app/src/sass/posts.scss new file mode 100755 index 0000000..a42c6d3 --- /dev/null +++ b/website/backend/app/src/sass/posts.scss @@ -0,0 +1,109 @@ +$spacing: 10px; + +.thread_container, .post_container { + margin: $spacing 0 0 0; + padding: $spacing; +} + +.thread_container { + overflow: visible; + + .op, .post_container { + position: relative; + } + + .op { + .post_admin { + margin-left: $spacing * 35; + } + } + + .post_container { + display: table; + min-width: 300px; + max-width: 100%; + + .post_admin { + margin-left: $spacing * 2; + } + } + + .post_file { + margin: 0 0 3px 0; + + a { + text-decoration: underline; + } + } + + .post_image { + float: left; + margin: 0 $spacing * 2 0 $spacing; + + img { + max-width: 1000px; + max-height: 1000px; + } + } + + .post_subject, .post_name { + font-weight: bold; + } + + .post_comment { + padding: $spacing $spacing * 1.5; + + a { + text-decoration: underline; + } + + .broken_link { + text-decoration: line-through; + } + + .spoiler { + background-color: #000000; + color: #000000; + + &:hover { + color: #ffffff; + } + } + + .post_banned { + color: #ff0000; + font-weight: bold; + margin-top: $spacing * 2; + } + } + + .post_flag, .post_menu { + cursor: pointer; + } + + .post_menu { + position: relative; + display: inline-block; + + .menu { + display: none; + position: absolute; + z-index: 9; + + div { + padding: 3px; + + form { + margin: 0; + padding: 0; + + input[type="submit"], button { + background: none; + border: none; + padding: 0; + } + } + } + } + } +} diff --git a/website/backend/app/src/sass/reset.scss b/website/backend/app/src/sass/reset.scss new file mode 100755 index 0000000..887bf88 --- /dev/null +++ b/website/backend/app/src/sass/reset.scss @@ -0,0 +1,141 @@ +/* + HTML5 Reset :: style.css + ---------------------------------------------------------- + We have learned much from/been inspired by/taken code where offered from: + + Eric Meyer :: http://meyerweb.com + HTML5 Doctor :: http://html5doctor.com + and the HTML5 Boilerplate :: http://html5boilerplate.com + +-------------------------------------------------------------------------------*/ + +/* Let's default this puppy out +-------------------------------------------------------------------------------*/ + +html, body, body div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, figure, footer, header, menu, nav, section, time, mark, audio, video, details, summary { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font-weight: normal; + vertical-align: baseline; + background: transparent; +} + +article, aside, figure, footer, header, nav, section, details, summary {display: block;} + +/* Handle box-sizing while better addressing child elements: + http://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */ +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +/* consider resetting the default cursor: https://gist.github.com/murtaugh/5247154 */ + +/* Responsive images and other embedded objects */ +/* if you don't have full control over `img` tags (if you have to overcome attributes), consider adding height: auto */ +img, +object, +embed {max-width: 100%;} + +/* + Note: keeping IMG here will cause problems if you're using foreground images as sprites. + In fact, it *will* cause problems with Google Maps' controls at small size. + If this is the case for you, try uncommenting the following: + +#map img { + max-width: none; +} +*/ + +/* force a vertical scrollbar to prevent a jumpy page */ +html {overflow-y: scroll;} + +/* we use a lot of ULs that aren't bulleted. + you'll have to restore the bullets within content, + which is fine because they're probably customized anyway */ +ul {list-style: none;} + +blockquote, q {quotes: none;} + +blockquote:before, +blockquote:after, +q:before, +q:after {content: ''; content: none;} + +a {margin: 0; padding: 0; font-size: 100%; vertical-align: baseline; background: transparent;} + +del {text-decoration: line-through;} + +abbr[title], dfn[title] {border-bottom: 1px dotted #000; cursor: help;} + +/* tables still need cellspacing="0" in the markup */ +table {border-collapse: collapse; border-spacing: 0;} +th {font-weight: bold; vertical-align: bottom;} +td {font-weight: normal; vertical-align: top;} + +hr {display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0;} + +input, select {vertical-align: middle;} + +pre { + white-space: pre; /* CSS2 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ +} + +input[type="radio"] {vertical-align: text-bottom;} +.ie7 input[type="checkbox"] {vertical-align: baseline;} +.ie6 input {vertical-align: text-bottom;} + +select, input, textarea {font: 99% sans-serif;} + +table {font-size: inherit; font: 100%;} + +small {font-size: 85%;} + +strong {font-weight: bold;} + +td, td img {vertical-align: top;} + +/* Make sure sup and sub don't mess with your line-heights http://gist.github.com/413930 */ +sub, sup {font-size: 75%; line-height: 0; position: relative;} +sup {top: -0.5em;} +sub {bottom: -0.25em;} + +/* standardize any monospaced elements */ +pre, code, kbd, samp {font-family: monospace, sans-serif;} + +/* hand cursor on clickable elements */ +.clickable, +input[type=button], +input[type=submit], +input[type=file], +button {cursor: pointer;} + +/* Webkit browsers add a 2px margin outside the chrome of form elements */ +button, input, select, textarea {margin: 0;} + +/* make buttons play nice in IE */ +button, +input[type=button] {width: auto; overflow: visible;} + +/* scale images in IE7 more attractively */ +.ie7 img {-ms-interpolation-mode: bicubic;} + +/* prevent BG image flicker upon hover + (commented out as usage is rare, and the filter syntax messes with some pre-processors) +.ie6 html {filter: expression(document.execCommand("BackgroundImageCache", false, true));} +*/ + +/* let's clear some floats */ +.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } +.clearfix:after { clear: both; } +.clearfix { zoom: 1; } diff --git a/website/backend/app/src/sass/style.scss b/website/backend/app/src/sass/style.scss new file mode 100755 index 0000000..83fae1f --- /dev/null +++ b/website/backend/app/src/sass/style.scss @@ -0,0 +1,322 @@ +@import "reset"; +@import "posts"; + +@mixin display-flex { + display: -webkit-box-flex; + display: -webkit-flexbox; + display: -ms-flexbox; + display: -webkit-box; + display: flex; +} + +@mixin flex($args) { + -webkit-flex: $args; + -ms-flex: $args; + flex: $args; +} + +@mixin flex-direction($dir) { + $alt: vertical !default; + @if $dir == column { + $alt: vertical; + } @else { + $alt: horizontal; + } + -webkit-flex-direction: column; + -webkit-box-orient: $alt; + -ms-flex-direction: column; + flex-direction: column; +} + +$spacing: 10px; + +body { + margin: $spacing; + font-size: 10pt; + font-family: Helvetica, "Noto Sans", "Roboto Sans", Arial, sans-serif; +} + +a { + text-decoration: none; +} + +input[type="text"], +input[type="password"], +textarea, +select, +.admin_main { + padding: 4px; + width: 280px; +} + +input[type="checkbox"] { + margin: 4px 0; +} + +input[type="file"] { + padding: 4px 0; +} + +h1 { + font-size: 240%; + font-weight: bolder; + margin-top: 10px; + text-align: center; +} + +h2 { + margin: $spacing 0; + text-align: center; +} + +hr { + clear: left; +} + +form { + display: inline-block; + margin: $spacing / 2 auto; + padding: $spacing; +} + +table { + border-collapse: separate; + border-spacing: 1px; + margin: 0 auto; + max-width: 80%; + + thead { + vertical-align: middle; + + td { + font-weight: bolder; + padding: $spacing / 2 $spacing; + } + } + + td { + padding: 3px $spacing / 2; + text-align: center; + } + + .rules { + text-align: left; + } +} + +ul { + list-style: disc; + + li { + margin-left: $spacing * 2; + } +} + +ol { + list-style: decimal; + + li { + margin-left: $spacing * 2; + } +} + +#error { + background-color: #ff0000; + color: #ffffff; + margin-bottom: $spacing; + padding: $spacing; + text-align: center; + + h3 { + font-size: 220%; + font-weight: bolder; + + } + + p { + font-weight: bold; + } +} + +#post_form, .admin_form { + text-align: center; + + form { + div { + @include display-flex; + + .title { + @include flex(0); + font-weight: bold; + text-align: right; + min-width: 120px; + margin: 1px; + padding: 4px; + + span { + cursor: pointer; + } + } + + .fields { + margin: 1px; + + textarea { + min-height: 100px !important; + } + } + } + + button { + margin: $spacing 0 0 0; + } + } +} + +.admin_form { + form { + margin: 0; + padding: 0; + + button { + margin: 0; + } + } +} + +.required { + color: #ff0000; +} + +.boards, .return, .copyright { + text-align: center; +} + +.archive_stats { + text-align: center; + margin-bottom: $spacing; +} + +.announcement { + text-align: center; + font-weight: bold; +} + +.banned { + border: 7px dashed #000000; + display: table; + font-size: 180%; + min-width: 200px; + max-width: 600px; + margin: $spacing * 2 auto; + padding: $spacing; + + p { + margin: $spacing; + } + + div { + text-align: center; + } +} + +.medium { + width: 120px !important; +} + +.short { + width: 40px !important; +} + +.catalog_container { + display: inline-block; + overflow: hidden; + text-align: center; + vertical-align: top; + width: 135px; + + .catalog_stats { + font-size: 70%; + } + + img { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + max-width: 125px; + max-height: 125px; + } +} + +.logout { + top: $spacing; + right: $spacing; + position: absolute; +} + +.copyright { + margin-top: $spacing; +} + +.front_page { + h2 { + font-size: 180%; + } + + h3 { + font-size: 140%; + margin-top: $spacing; + } + + .board { + display: inline-block; + max-width: 300px; + margin: $spacing * 2; + vertical-align: top; + + a { + font-size: 160%; + font-weight: bold; + } + + .subtext { + display: block; + font-size: 120%; + } + } +} + +.table_of_contents { + display: table; + margin: 0 auto; + max-width: 400px; +} + +.answers,{ + max-width: 600px; + margin: 0 auto; + + h2 { + font-size: 160%; + } +} + +.install { + max-width: 300px; + margin: 0 auto; + + h2 { + font-size: 160%; + margin-top: $spacing * 2; + } + + label { + font-weight: bold; + margin-top: $spacing * 2; + } + + button { + font-size: 120%; + font-weight: bold; + margin-top: $spacing * 2; + padding: $spacing / 2; + } +} diff --git a/website/backend/app/src/sass/yotsuba.scss b/website/backend/app/src/sass/yotsuba.scss new file mode 100755 index 0000000..5c6b253 --- /dev/null +++ b/website/backend/app/src/sass/yotsuba.scss @@ -0,0 +1,113 @@ +$background: #ffffee; +$bg_light: #ffffff; +$border: #d9bfb7; +$subject: #cc1105; +$header: #800000; +$link: #0000ee; +$hover: #ff0000; +$quote: #000080; +$post: #f0e0d6; +$name: #117743; +$black: #000000; +$green: #789922; +$blue: #292299; +$active: #f0c0b0; +$active_border: #d99f91; + +body { + background-color: $background; +} + +body, h1, h2 { + color: $header; +} + +a, a:visited, .broken_link { + color: $link; +} + +a:hover, .quote_link:hover, .broken_link:hover { + color: $hover; +} + +.quote_link { + color: $quote; +} + +.quote_green { + color: $green; +} + +.quote_blue { + color: $blue; +} + +.announcement { + color: $header; +} + +#post_form, .admin_form { + form { + .title { + background-color: $active; + border: 1px solid $active_border; + } + } +} + +table { + thead { + td { + background-color: $post; + border: 1px solid $border; + } + } + + tr:nth-of-type(odd) { + background-color: $background; + } + + tr:nth-of-type(even) { + background-color: $bg_light; + } +} + +.post_container { + background-color: $post; + border-right: 1px solid $border; + border-bottom: 1px solid $border; + + &:target { + background-color: $active; + border-right: 1px solid $active_border; + border-bottom: 1px solid $active_border; + } +} + +.post_subject { + color: $subject; +} + +.post_name, .post_trip { + color: $name; +} + +.post_menu .menu { + background-color: $post; + border:1px solid $border; + + div { + border-right: 1px solid $border; + border-bottom: 1px solid $border; + + form { + button { + color: $header; + + &:hover { + color: $hover; + } + } + } + } +} diff --git a/website/backend/app/src/sass/yotsuba_b.scss b/website/backend/app/src/sass/yotsuba_b.scss new file mode 100755 index 0000000..732c1db --- /dev/null +++ b/website/backend/app/src/sass/yotsuba_b.scss @@ -0,0 +1,109 @@ +$background: #eef2ff; +$bg_light: #f7f9ff; +$border: #b7c5d9; +$subject: #0f0c5d; +$header: #af0a0f; +$link: #34345c; +$hover: #dd0000; +$post: #d6daf0; +$name: #117743; +$black: #000000; +$green: #789922; +$blue: #292299; +$active: #d6bad0; +$active_border: #ba9dbf; + +body { + background-color: $background; + color: $black; +} + +h1, h2 { + color: $header; +} + +a, a:visited, .broken_link { + color: $link; +} + +a:hover, .quote_link, .quote_link:visited, .broken_link:hover { + color: $hover; +} + +.quote_green { + color: $green; +} + +.quote_blue { + color: $blue; +} + +.announcement { + color: $header; +} + +#post_form, .admin_form { + form { + .title { + background-color: $active; + border: 1px solid $active_border; + } + } +} + +table { + thead { + td { + background-color: $post; + border: 1px solid $border; + } + } + + tr:nth-of-type(odd) { + background-color: $background; + } + + tr:nth-of-type(even) { + background-color: $bg_light; + } +} + +.post_container { + background-color: $post; + border-right: 1px solid $border; + border-bottom: 1px solid $border; + + &:target { + background-color: $active; + border-right: 1px solid $active_border; + border-bottom: 1px solid $active_border; + } +} + +.post_subject { + color: $subject; +} + +.post_name, .post_trip { + color: $name; +} + +.post_menu .menu { + background-color: $post; + border:1px solid $border; + + div { + border-right: 1px solid $border; + border-bottom: 1px solid $border; + + form { + button { + color: $link; + + &:hover { + color: $hover; + } + } + } + } +} diff --git a/website/backend/app/src/utils/capture.lua b/website/backend/app/src/utils/capture.lua new file mode 100755 index 0000000..2b5816b --- /dev/null +++ b/website/backend/app/src/utils/capture.lua @@ -0,0 +1,35 @@ +local ngx = _G.ngx +local json = require "cjson" + +local function capture(method, uri, body) + local response = ngx.location.capture(uri, { + method = method, + body = json.encode(body) + }) + + if response.truncated then return end + + if response.status ~= ngx.HTTP_OK then + return nil, json.decode(response.body) + end + + return json.decode(response.body) +end + +return { + get = function(...) + return capture(ngx.HTTP_GET, ...) + end, + + post = function(...) + return capture(ngx.HTTP_POST, ...) + end, + + put = function(...) + return capture(ngx.HTTP_PUT, ...) + end, + + delete = function(...) + return capture(ngx.HTTP_DELETE, ...) + end, +} diff --git a/website/backend/app/src/utils/error.lua b/website/backend/app/src/utils/error.lua new file mode 100755 index 0000000..c6644bc --- /dev/null +++ b/website/backend/app/src/utils/error.lua @@ -0,0 +1,104 @@ +local ngx = _G.ngx +local get_error = {} +local status = {} + +--[[ API Error Codes ]]-- + +-- Authorization + +-- email:api_key format in Authorization HTTP header is invalid +function get_error.malformed_authorization() + return { code=100 } +end + +-- email:api_key in Authorization HTTP header does not match any user +-- login credentials do not match any user +function get_error.invalid_authorization() + return { code=101 } +end + +-- Attempting to access endpoint that requires higher priviliges +function get_error.unauthorized_access() + return { code=102 } +end + +-- Data Validation + +function get_error.field_not_found(field) + return { code=200, field=field } +end +function get_error.field_invalid(field) + return { code=201, field=field } +end +function get_error.field_not_unique(field) + return { code=202, field=field } +end +function get_error.token_expired(field) + return { code=203, field=field } +end +function get_error.password_not_match() + return { code=204 } +end + +-- Database I/O + +function get_error.database_unresponsive() + return { code=300 } +end +function get_error.database_create() + return { code=301 } +end +function get_error.database_modify() + return { code=302 } +end +function get_error.database_delete() + return { code=303 } +end +function get_error.database_select() + return { code=304 } +end + +--[[ API -> HTTP Code Map ]]-- + +-- Authorization +status[100] = ngx.HTTP_BAD_REQUEST +status[101] = ngx.HTTP_FORBIDDEN +status[102] = ngx.HTTP_UNAUTHORIZED + +-- Data Validation +status[200] = ngx.HTTP_BAD_REQUEST +status[201] = ngx.HTTP_BAD_REQUEST +status[202] = ngx.HTTP_BAD_REQUEST +status[203] = ngx.HTTP_BAD_REQUEST + +-- Database I/O +status[300] = ngx.HTTP_INTERNAL_SERVER_ERROR +status[301] = ngx.HTTP_INTERNAL_SERVER_ERROR +status[302] = ngx.HTTP_INTERNAL_SERVER_ERROR +status[303] = ngx.HTTP_INTERNAL_SERVER_ERROR +status[304] = ngx.HTTP_INTERNAL_SERVER_ERROR + +return { + get_error = get_error, + handle = function(self) + + -- Inject localized error messages + for _, err in ipairs(self.errors) do + --err.message = self.i18n(err.code) + if type(err) == "table" then + for k, v in pairs(err) do + print(k, ": ", v) + end + else + print(err) + end + end + + print(#self.errors) + + return self:write { + status = 401,--status[self.errors[1].code], + json = self.errors + } + end +} diff --git a/website/backend/app/src/utils/file_whitelist.lua b/website/backend/app/src/utils/file_whitelist.lua new file mode 100755 index 0000000..78aa552 --- /dev/null +++ b/website/backend/app/src/utils/file_whitelist.lua @@ -0,0 +1,22 @@ +-- A whitelist of filetypes +return { + -- Image formats + image = { + [".bmp"] = true, + [".png"] = true, + [".gif"] = true, + [".jpg"] = true, + [".jpeg"] = true, + [".webp"] = true, + [".webm"] = true, + [".svg"] = true + }, + + -- Audio formats + audio = { + [".wav"] = true, + [".flac"] = true, + [".mp3"] = true, + [".ogg"] = true + } +} diff --git a/website/backend/app/src/utils/generate.lua b/website/backend/app/src/utils/generate.lua new file mode 100755 index 0000000..2dd32a4 --- /dev/null +++ b/website/backend/app/src/utils/generate.lua @@ -0,0 +1,94 @@ + +local config = require("lapis.config").get() +local encoding = require "lapis.util.encoding" +local sha256 = require "resty.sha256" +local ffi = require "ffi" +local posix = require "posix" +local salt = loadfile("../data/secrets/salt.lua")() +local token = config.secret +local sf = string.format +local ss = string.sub + +local function get_chunks(str) + -- Secure trip + local name, tripcode = str:match("(.-)(##.+)") + + -- Insecure trip + if not name then + name, tripcode = str:match("(.-)(#.+)") + + -- Just a name + if not name then + return str:match("(.+)") + end + end + + return name, tripcode +end + +local generate = {} + +-- math.random isn't reliable for this use case, so instead we're gonna snag +-- some bytes from /dev/urandom, create a uint32, and grab the last 3 digits. +function generate.random() + -- Read uint32_t from /dev/urandom + local r = io.open("/dev/urandom", "rb") + local bytes = r:read(4) + r:close() + + -- Build number + local num = ffi.new("unsigned int[1]") + ffi.copy(num, bytes, 4) + + return sf("%03d", num[0] % 1000) +end + +-- Generate an insecure password +function generate.password(time) + local hasher = sha256:new() + hasher:update(sf("%s%s", time, generate.random())) + return encoding.encode_base64(hasher:final()) +end + +-- Generate a secure or insecure tripcode based off the name a user supplies +-- when they make a post. #trip for insecure, ##trip for secure. +-- Secure tripcodes use sha256 + the app's secret token. +-- Insecure tripcodes use standard posix crypt + the app's secret salt. +function generate.tripcode(raw_name) + local name, tripcode = get_chunks(raw_name) + + if tripcode then + local pattern = "^([^=]*)" + tripcode = tripcode:sub(2) -- remove leading '#' + + -- Secure tripcode + if tripcode:sub(1, 1) == "#" then + local hasher = sha256:new() + tripcode = token .. tripcode:sub(2) -- remove leading '#' + hasher:update(tripcode) + local hash = encoding.encode_base64(hasher:final()) + tripcode = "!!" .. ss(hash:match(pattern), -10) + -- Insecure tripcode + else + local hash = posix.crypt(tripcode, salt) + tripcode = "!" .. ss(hash, -10) + end + end + + return name, tripcode +end + +function generate.errors(i18n, errors) + local err = {} + + if #errors > 0 then + for _, error in ipairs(errors) do + local e = i18n(unpack(error)) + table.insert(err, e) + end + end + + return err +end + +return generate diff --git a/website/backend/app/src/utils/request_processor.lua b/website/backend/app/src/utils/request_processor.lua new file mode 100755 index 0000000..3e4cec7 --- /dev/null +++ b/website/backend/app/src/utils/request_processor.lua @@ -0,0 +1,298 @@ +local Bans = require "models.bans" +local Threads = require "models.threads" +local Posts = require "models.posts" +local Reports = require "models.reports" +local process = {} + +function process.create_thread(params, session, board) + + -- Prepare data for entry + local _, err = Posts:prepare_post(params, session, board) + if err then + return false, err + end + + -- Archive old threads + local max_threads = board.threads_per_page * board.pages + Threads:archive_threads(board.id, max_threads) + + -- Delete old archived threads + local time = os.time() + local threads = Threads:get_archived_threads(board.id) + + for _, t in ipairs(threads) do + if time - t.last_active > board.archive_time and not t.save then + local posts = Posts:get_thread_posts(t.id) + Threads:delete_thread("override", t, posts[1]) + + -- Delete all associated posts + for _, post in ipairs(posts) do + Posts:delete_post("override", board, post) + + -- Delete associated report + local report = Reports:get_report(post.id) + if report then + Reports:delete_report(report) + end + end + end + end + + -- Insert post data into database + local post, err = Posts:create_post( + params, + session, + board, + thread, + true + ) + if err then + return false, err + end + + return post +end + +function process.create_post(params, session, board, thread) + local posts = Posts:count_posts(thread.id) + local files = Posts:count_files(thread.id) + + -- Prepare data for entry + local _, err = Posts:prepare_post( + params, session, board, thread, files + ) + if err then + return false, err + end + + -- Insert post data into database + local post, err = Posts:create_post( + params, + session, + board, + thread, + false + ) + if err then + return false, err + end + + posts = posts + 1 + + -- Check for [auto]sage + if params.options ~= "sage" and + posts <= board.post_limit then + -- Update thread + thread.last_active = os.time() + thread:update("last_active") + end + + return post +end + +function process.delete_thread(params, session, board) + -- Validate post + local post = Posts:get_post(board.id, params.thread_id) + if not post then + return false, { "err_invalid_post", { params.thread_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + local posts = Posts:get_posts_by_thread(thread.id) + + -- Delete thread + local _, err = Threads:delete_thread(session, thread, posts[1]) + if err then + return false, err + end + + -- Delete all associated posts + for _, post in ipairs(posts) do + Posts:delete_post("override", board, post) + + -- Delete associated report + local report = Reports:get_report(board.id, post.id) + if report then + Reports:delete_report(report) + end + end + + return true +end + +function process.delete_post(params, session, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + -- Delete post + local _, err = Posts:delete_post(session, board, post) + if err then + return false, err + end + + -- Update thread + local posts = Posts:get_posts_by_thread(thread.id) + thread.last_active = posts[#posts].timestamp + thread:update("last_active") + + return true +end + +function process.report_post(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.thread } } + end + + local report = Reports:get_report(board.id, post.post_id) + + -- If report exists, update it + if report then + report.num_reports = report.num_reports + 1 -- FIXME: race condition + local _, err = Reports:modify_report(report) + if err then + return false, err + end + -- If report is new, create it + else + local _, err = Reports:create_report { + board_id = board.id, + thread_id = post.thread_id, + post_id = post.post_id, + timestamp = os.time(), + num_reports = 1 + } + if err then + return false, err + end + end + + return post +end + +-- Sticky thread +function process.sticky_thread(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + thread.sticky = not thread.sticky + thread:update("sticky") + + return true +end + +-- Lock thread +function process.lock_thread(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + thread.lock = not thread.lock + thread:update("lock") + + return true +end + +-- Save thread +function process.save_thread(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + thread.save = not thread.save + thread:update("save") + + return true +end + +-- Override thread +function process.override_thread(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + -- Validate thread + local thread = Threads:get_thread(post.thread_id) + if not thread then + return false, { "err_invalid_thread" } + end + + thread.size_override = not thread.size_override + thread:update("size_override") + + return true +end + +-- Ban user +function process.ban_user(params, board) + -- Validate post + local post = Posts:get_post(board.id, params.post_id) + if not post then + return false, { "err_invalid_post", { params.post_id } } + end + + params.ip = post.ip + + -- Convert board name to id if checkbox is set + if params.board_id then + params.board_id = board.id + end + + -- Ban user + local _, err = Bans:create_ban(params) + if err then + return false, err + end + + -- Flag post + if params.banned then + post.banned = true + post:update("banned") + end + + return true +end + +return process diff --git a/website/backend/app/src/utils/role.lua b/website/backend/app/src/utils/role.lua new file mode 100755 index 0000000..87062ce --- /dev/null +++ b/website/backend/app/src/utils/role.lua @@ -0,0 +1,31 @@ +local models = require "models" +local get_error = require "utils.error".get_error +local Users = models.users +local role = {} + +-- User must be the Owner +function role.owner(user) + return user.role == Users.role.OWNER and true or nil, get_error.unauthorized_access() +end + +-- User must be an Admin or higher +function role.admin(user) + return user.role >= Users.role.ADMIN and true or nil, get_error.unauthorized_access() +end + +-- User must be a Mod or higher +function role.mod(user) + return user.role >= Users.role.MOD and true or nil, get_error.unauthorized_access() +end + +-- User must be a Janitor or higher +function role.janitor(user) + return user.role >= Users.role.JANITOR and true or nil, get_error.unauthorized_access() +end + +-- User must be signed in +function role.user(user) + return user.role >= Users.role.USER and true or nil, get_error.unauthorized_access() +end + +return role diff --git a/website/backend/app/src/utils/text_formatter.lua b/website/backend/app/src/utils/text_formatter.lua new file mode 100755 index 0000000..ae1f44a --- /dev/null +++ b/website/backend/app/src/utils/text_formatter.lua @@ -0,0 +1,160 @@ +local Posts = require "models.posts" +local escape = require("lapis.html").escape +local sf = string.format +local formatter = {} + +--- Sanitize text for HTML safety +-- @tparam string text Raw text +-- @treturn string formatted +function formatter.sanitize(text) + return escape(text) +end + +--- Format new lines to 'br' tags +-- @tparam string text Raw text +-- @treturn string formatted +function formatter.new_lines(text) + return text:gsub("\n", "
\n") +end + +--- Format words that begin with '>>' +-- @tparam string text Raw text +-- @tparam table request Request object +-- @tparam table board Board data +-- @tparam table post Post data +-- @treturn string formatted +function formatter.quote(text, request, board, post) + local function get_url(board, post_id) + if tonumber(post_id) then + local p = Posts:get(board.id, post_id) + if not p then return false end + + local thread = p:get_thread() + if not thread then return false end + + local op = thread:get_op() + return + request:url_for("web.boards.thread", { board=board.name, thread=op.post_id }), + op + else + return request:url_for("web.boards.board", { board=board.name }) + end + end + + -- >>1234 ur a fag + -- >>(%d+) + local match_pattern = ">>(%d+)" + local sub_pattern = ">>%s" + + -- Get all the matches and store them in an ordered list + local posts = {} + for post_id in text:gmatch(match_pattern) do + table.insert(posts, { board=board, id=post_id }) + end + + -- Format each match + for i, p in ipairs(posts) do + local text = sf(sub_pattern, p.id) + local url, op = get_url(p.board, p.id) + if url then + if op.thread_id == post.thread_id then + posts[i] = sf("%s", url, p.id, text) + else + posts[i] = sf("%s→", url, p.id, text) + end + else + posts[i] = sf("%s", text) + end + end + + -- Substitute each match with the formatted match + local i = 0 + text = text:gsub(match_pattern, function() + i = i + 1 + return posts[i] + end) + + -- >>>/a/1234 check over here + -- >>>/(%w+)/(%d*) + match_pattern = ">>>/(%w+)/(%d*)" + sub_pattern = ">>>/%s/%s" + + -- Get all the matches and store them in an ordered list + posts = {} + for b, post_id in text:gmatch(match_pattern) do + local response = request.api.boards.GET(request) + b = response.json or b + table.insert(posts, { board=b, id=post_id }) + end + + -- Format each match + for i, p in ipairs(posts) do + if type(p.board) == "table" then + local text = sf(sub_pattern, p.board.name, p.id) + local url, op = get_url(p.board, p.id) + if op then + posts[i] = sf("%s", url, p.id, text) + else + posts[i] = sf("%s", url, text) + end + else + local text = sf(sub_pattern, p.board, p.id) + posts[i] = sf("%s", text) + end + end + + -- Substitute each match with the formatted match + i = 0 + text = text:gsub(match_pattern, function() + i = i + 1 + return posts[i] + end) + + return text +end + +--- Format lines that begin with '>' +-- @tparam string text Raw text +-- @treturn string formatted +function formatter.green_text(text) + local formatted = "" + + for line in text:gmatch("[^\n]+") do + local first = line:sub(1, 4) + + -- >implying + if first == ">" then + line = sf("%s%s%s", "", line, "") + end + + formatted = sf("%s%s%s", formatted, line, "\n") + end + + return formatted +end + +--- Format lines that begin with '<' +-- @tparam string text Raw text +-- @treturn string formatted +function formatter.blue_text(text) + local formatted = "" + + for line in text:gmatch("[^\n]+") do + local first = line:sub(1, 4) + + -- ", line, "") + end + + formatted = sf("%s%s%s", formatted, line, "\n") + end + + return formatted +end + +function formatter.spoiler(text) + return text:gsub("(%[spoiler%])(.-)(%[/spoiler%])", "%2") +end + +return formatter diff --git a/website/backend/app/src/views/admin/admin.etlua b/website/backend/app/src/views/admin/admin.etlua new file mode 100755 index 0000000..0619f36 --- /dev/null +++ b/website/backend/app/src/views/admin/admin.etlua @@ -0,0 +1,247 @@ +

<%= page_title %>

+ +
+ + +
+
+ + +
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+ +
+ + + + +
+
+
+ +
diff --git a/website/backend/app/src/views/admin/announcement.etlua b/website/backend/app/src/views/admin/announcement.etlua new file mode 100755 index 0000000..780c76d --- /dev/null +++ b/website/backend/app/src/views/admin/announcement.etlua @@ -0,0 +1,30 @@ +
+
+ + +
+ + + + +
+ +
+ + + + +
+ + <% if params.action == "create" then %> + + <% elseif params.action == "modify" then %> + + <% end %> +
+
diff --git a/website/backend/app/src/views/admin/board.etlua b/website/backend/app/src/views/admin/board.etlua new file mode 100755 index 0000000..9248868 --- /dev/null +++ b/website/backend/app/src/views/admin/board.etlua @@ -0,0 +1,189 @@ +
+
+ + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + <% if params.action == "create" then %> + + <% elseif params.action == "modify" then %> + + <% end %> +
+
diff --git a/website/backend/app/src/views/admin/login.etlua b/website/backend/app/src/views/admin/login.etlua new file mode 100755 index 0000000..7194224 --- /dev/null +++ b/website/backend/app/src/views/admin/login.etlua @@ -0,0 +1,21 @@ +
+
+ + +
+ + + + +
+ +
+ + + + +
+ + +
+
diff --git a/website/backend/app/src/views/admin/page.etlua b/website/backend/app/src/views/admin/page.etlua new file mode 100755 index 0000000..4ab4e75 --- /dev/null +++ b/website/backend/app/src/views/admin/page.etlua @@ -0,0 +1,33 @@ +
+
+ + + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + <% if params.action == "create" then %> + + <% elseif params.action == "modify" then %> + + <% end %> +
+
diff --git a/website/backend/app/src/views/admin/success.etlua b/website/backend/app/src/views/admin/success.etlua new file mode 100755 index 0000000..2e96583 --- /dev/null +++ b/website/backend/app/src/views/admin/success.etlua @@ -0,0 +1,3 @@ +

Success

+

<%= action %>

+[<%= i18n("return") %>] diff --git a/website/backend/app/src/views/admin/user.etlua b/website/backend/app/src/views/admin/user.etlua new file mode 100755 index 0000000..dd0a932 --- /dev/null +++ b/website/backend/app/src/views/admin/user.etlua @@ -0,0 +1,71 @@ +
+
+ + +
+ + + + +
+ + <% if params.action == "modify" then %> +
+ + + + +
+ <% end %> + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + <% if params.action == "create" then %> + + <% elseif params.action == "modify" then %> + + <% end %> +
+
diff --git a/website/backend/app/src/views/archive.etlua b/website/backend/app/src/views/archive.etlua new file mode 100755 index 0000000..a5d7a57 --- /dev/null +++ b/website/backend/app/src/views/archive.etlua @@ -0,0 +1,49 @@ +<% render('views.fragments.board_title') %> +<% render('views.fragments.announcements') %> +
+[<%= i18n('return') %>] +[<%= i18n('catalog') %>] +[<%= i18n('bottom') %>] +
+

+ + <%= i18n('arc_display', { + n_thread = #threads, + p_thread = i18n('threads', { count=#threads }), + n_day = days, + p_day = i18n('days', { count=days }) + }) %> + +

+ + + + + + + + + + + + <% for _, thread in ipairs(threads) do %> + + + + + + + + <% end %> + +
<%= i18n('arc_number') %><%= i18n('arc_name') %><%= i18n('arc_excerpt') %><%= i18n('arc_replies') %>
<%= thread.op.post_id %><%= thread.op.name %> <%= thread.op.trip %> + <% if thread.op.subject then %> + <%= thread.op.subject %>: + <% end %> + <%- thread.op.comment %> + <%= thread.replies %>[<%= i18n('arc_view') %>]
+
+[<%= i18n('return') %>] +[<%= i18n('catalog') %>] +[<%= i18n('top') %>] +
diff --git a/website/backend/app/src/views/banned.etlua b/website/backend/app/src/views/banned.etlua new file mode 100755 index 0000000..c94fc89 --- /dev/null +++ b/website/backend/app/src/views/banned.etlua @@ -0,0 +1,8 @@ +

<%= page_title %>

+
+

<%= i18n('ban_reason') %>

+

<%= reason %>

+

<%- i18n('ban_expire', { expire=''..expire..'' }) %>

+

<%- i18n('ban_ip', { ip=''..ip..'' }) %>

+
Catfish.
+
diff --git a/website/backend/app/src/views/board.etlua b/website/backend/app/src/views/board.etlua new file mode 100755 index 0000000..8590492 --- /dev/null +++ b/website/backend/app/src/views/board.etlua @@ -0,0 +1,36 @@ +<% render('views.fragments.board_title') %> +<% render('views.fragments.form_submit') %> +<% render('views.fragments.announcements') %> +
+[<%= i18n('catalog') %>] +[<%= i18n('archive') %>] +[<%= i18n('refresh') %>] +
+<% for _, thread in ipairs(threads) do %> +
+<% + local posts = thread.posts or {} + for i=#posts, 1, -1 do + local post = posts[i] + local op = posts[#posts] + if post.post_id == op.post_id then + render('views.fragments.op_content', { thread=thread, post=post, posts=posts, is_board=true }) + else + render('views.fragments.post_content', { thread=thread, post=post, posts=posts, is_board=true, op=op }) + end + end +%> +
+
+<% end %> +
+ <% for i=1, pages do %> + <% if i == params.page then %> + [<%= i %>] + <% else %> + [<%= i %>] + <% end %> + <% end %> + [<%= i18n('catalog') %>] + [<%= i18n('archive') %>] +
diff --git a/website/backend/app/src/views/catalog.etlua b/website/backend/app/src/views/catalog.etlua new file mode 100755 index 0000000..108ea8d --- /dev/null +++ b/website/backend/app/src/views/catalog.etlua @@ -0,0 +1,34 @@ +<% render('views.fragments.board_title') %> +<% render('views.fragments.form_submit') %> +<% render('views.fragments.announcements') %> +
+[<%= i18n('return') %>] +[<%= i18n('archive') %>] +[<%= i18n('bottom') %>] +[<%= i18n('refresh') %>] +
+<% for t, thread in ipairs(threads) do %> + +<% end %> +
+[<%= i18n('return') %>] +[<%= i18n('archive') %>] +[<%= i18n('top') %>] +[<%= i18n('refresh') %>] +
diff --git a/website/backend/app/src/views/code_404.etlua b/website/backend/app/src/views/code_404.etlua new file mode 100755 index 0000000..bd5f8f1 --- /dev/null +++ b/website/backend/app/src/views/code_404.etlua @@ -0,0 +1,2 @@ +

<%= page_title %>

+

<%= i18n("return_index") %>

diff --git a/website/backend/app/src/views/fragments/announcements.etlua b/website/backend/app/src/views/fragments/announcements.etlua new file mode 100755 index 0000000..9a4f08c --- /dev/null +++ b/website/backend/app/src/views/fragments/announcements.etlua @@ -0,0 +1,6 @@ +<% if #announcements > 0 then %> +
+<% for _, announcement in ipairs(announcements) do %> +

<%= announcement.text %>

+<% end %> +<% end %> diff --git a/website/backend/app/src/views/fragments/board_title.etlua b/website/backend/app/src/views/fragments/board_title.etlua new file mode 100755 index 0000000..0c6358b --- /dev/null +++ b/website/backend/app/src/views/fragments/board_title.etlua @@ -0,0 +1,6 @@ +
+

<%= page_title %>

+ <% if board.subtext then %> +

<%= board.subtext %>

+ <% end %> +
diff --git a/website/backend/app/src/views/fragments/copyright.etlua b/website/backend/app/src/views/fragments/copyright.etlua new file mode 100755 index 0000000..2a06a9d --- /dev/null +++ b/website/backend/app/src/views/fragments/copyright.etlua @@ -0,0 +1,9 @@ + diff --git a/website/backend/app/src/views/fragments/error.etlua b/website/backend/app/src/views/fragments/error.etlua new file mode 100755 index 0000000..cb0bdb3 --- /dev/null +++ b/website/backend/app/src/views/fragments/error.etlua @@ -0,0 +1,6 @@ +
+

/!\ ALART /!\

+<% for _, err in ipairs(errors) do %> +

<%= err %>

+<% end %> +
diff --git a/website/backend/app/src/views/fragments/form_ban.etlua b/website/backend/app/src/views/fragments/form_ban.etlua new file mode 100755 index 0000000..f4d72bf --- /dev/null +++ b/website/backend/app/src/views/fragments/form_ban.etlua @@ -0,0 +1,15 @@ +
+ + + <% if post.thread then %> + + <% end %> + + <%= i18n("form_ban_display") %> +
+ <%= i18n("form_ban_board") %> +
+ " /> + " />
+ +
diff --git a/website/backend/app/src/views/fragments/form_delete.etlua b/website/backend/app/src/views/fragments/form_delete.etlua new file mode 100755 index 0000000..2a3855c --- /dev/null +++ b/website/backend/app/src/views/fragments/form_delete.etlua @@ -0,0 +1,9 @@ +
+ + + <% if post.thread then %> + + <% end %> + + +
diff --git a/website/backend/app/src/views/fragments/form_locale.etlua b/website/backend/app/src/views/fragments/form_locale.etlua new file mode 100755 index 0000000..456cbb4 --- /dev/null +++ b/website/backend/app/src/views/fragments/form_locale.etlua @@ -0,0 +1,10 @@ +
+
+ + +
+
diff --git a/website/backend/app/src/views/fragments/form_lock.etlua b/website/backend/app/src/views/fragments/form_lock.etlua new file mode 100755 index 0000000..29693d2 --- /dev/null +++ b/website/backend/app/src/views/fragments/form_lock.etlua @@ -0,0 +1,9 @@ +
+ + + <% if post.thread then %> + + <% end %> + + +
diff --git a/website/backend/app/src/views/fragments/form_override.etlua b/website/backend/app/src/views/fragments/form_override.etlua new file mode 100755 index 0000000..1d19005 --- /dev/null +++ b/website/backend/app/src/views/fragments/form_override.etlua @@ -0,0 +1,9 @@ +
+ + + <% if post.thread then %> + + <% end %> + + +
diff --git a/website/backend/app/src/views/fragments/form_remix.etlua b/website/backend/app/src/views/fragments/form_remix.etlua new file mode 100755 index 0000000..6bfae28 --- /dev/null +++ b/website/backend/app/src/views/fragments/form_remix.etlua @@ -0,0 +1,9 @@ +<% if thread then %> +
+ +
+<% else %> +
+ +
+<% end %> diff --git a/website/backend/app/src/views/fragments/form_report.etlua b/website/backend/app/src/views/fragments/form_report.etlua new file mode 100755 index 0000000..d1bc1a6 --- /dev/null +++ b/website/backend/app/src/views/fragments/form_report.etlua @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/website/backend/app/src/views/fragments/form_save.etlua b/website/backend/app/src/views/fragments/form_save.etlua new file mode 100755 index 0000000..734841f --- /dev/null +++ b/website/backend/app/src/views/fragments/form_save.etlua @@ -0,0 +1,9 @@ +
+ + + <% if post.thread then %> + + <% end %> + + +
diff --git a/website/backend/app/src/views/fragments/form_sticky.etlua b/website/backend/app/src/views/fragments/form_sticky.etlua new file mode 100755 index 0000000..94e144b --- /dev/null +++ b/website/backend/app/src/views/fragments/form_sticky.etlua @@ -0,0 +1,9 @@ +
+ + + <% if post.thread then %> + + <% end %> + + +
diff --git a/website/backend/app/src/views/fragments/form_submit.etlua b/website/backend/app/src/views/fragments/form_submit.etlua new file mode 100755 index 0000000..46ce1da --- /dev/null +++ b/website/backend/app/src/views/fragments/form_submit.etlua @@ -0,0 +1,144 @@ +
+
+ + + <% if params.thread then %> + + <% end %> + +
+ + + + +
+ + <% if not params.thread then %> +
+ + + + +
+ <% end %> + +
+ + + + +
+ +
+ + + + +
+ + <% if not board.text_only and (not num_files or num_files < board.thread_file_limit) then %> +
+ + + + +
+ + <% if board.draw then %> +
+ + + + <%= i18n("form_width") %>: + + <%= i18n("form_height") %>: + + + + +
+ <% end %> + +
+ + + [ + /> + ] + +
+ <% end %> + + <% if not params.thread and (session.admin or session.mod) then %> +
+ + + + [ + ">📌 + /> + ] + + + [ + ">🔒 + /> + ] + + + [ + ">💾 + /> + ] + + + [ + ">✔️ + /> + ] + + +
+ <% end %> + + +
+

+ * + <%= i18n("form_required") %> +

+

+ <%- + i18n("form_readme", { -- FIXME + rules = 'Rules', + faq = 'FAQ' + }) + %> +

+
diff --git a/website/backend/app/src/views/fragments/list_boards.etlua b/website/backend/app/src/views/fragments/list_boards.etlua new file mode 100755 index 0000000..d6ac290 --- /dev/null +++ b/website/backend/app/src/views/fragments/list_boards.etlua @@ -0,0 +1,16 @@ +<% if boards then %> +
+ [ <%= i18n('index') %> ] + <% local group = 1 %> + <% for i, board in ipairs(boards) do %> + <% local sub = sub_page or '' %> + <% if board.group ~= group then %> + <% group = board.group %> ] [ + <% else %> + <%= i == 1 and '[' or '/' %> + <% end %> + <%= board.name %> + <%= i == #boards and ']' or '' %> + <% end %> +
+<% end %> diff --git a/website/backend/app/src/views/fragments/op_content.etlua b/website/backend/app/src/views/fragments/op_content.etlua new file mode 100755 index 0000000..512dc90 --- /dev/null +++ b/website/backend/app/src/views/fragments/op_content.etlua @@ -0,0 +1,71 @@ +
+ <% if post.file_name then %> +
+ <%= i18n("file") %>: + <% + if post.file_name:len() > 35 then + local short = string.sub(post.file_name, 1, 25) + local ext = post.file_name:match("^.+(%..+)$") + local name = string.format("%s(...)%s", short, ext) + %> + <%= name %> + <% else %> + <%= post.file_name %> + <% end%> + (<%= post.file_size %> KB<%= post.file_dimensions %><%= post.file_duration %>) +
+
+ +
+ <% end %> + +
+ <%- post.comment %> + <% if post.banned then %> +

(<%= board.ban_message %>)

+ <% end %> +
+
+<% if thread.hidden and thread.hidden.posts > 0 then %> +
+ <%- + i18n("post_hidden", { + n_post = thread.hidden.posts, + p_post = i18n("posts", { count=thread.hidden.posts }), + n_file = thread.hidden.files, + p_file = i18n("files", { count=thread.hidden.files }), + click = "Click here" + }) + %> +
+<% end %> diff --git a/website/backend/app/src/views/fragments/post_content.etlua b/website/backend/app/src/views/fragments/post_content.etlua new file mode 100755 index 0000000..385bad2 --- /dev/null +++ b/website/backend/app/src/views/fragments/post_content.etlua @@ -0,0 +1,39 @@ +
+ + <% if post.file_name then %> +
+ <%= i18n("file") %>: + <% + if post.file_name:len() > 35 then + local short = string.sub(post.file_name, 1, 25) + local ext = post.file_name:match("^.+(%..+)$") + local name = string.format("%s(...)%s", short, ext) + %> + <%= name %> + <% else %> + <%= post.file_name %> + <% end%> + (<%= post.file_size %> KB<%= post.file_dimensions %><%= post.file_duration %>) +
+
+ +
+ <% end %> +
+ <%- post.comment %> + <% if post.banned then %> +

(<%= board.ban_message %>)

+ <% end %> +
+
diff --git a/website/backend/app/src/views/fragments/post_menu.etlua b/website/backend/app/src/views/fragments/post_menu.etlua new file mode 100755 index 0000000..acdbbee --- /dev/null +++ b/website/backend/app/src/views/fragments/post_menu.etlua @@ -0,0 +1,21 @@ +
+ + +
diff --git a/website/backend/app/src/views/fragments/return_board.etlua b/website/backend/app/src/views/fragments/return_board.etlua new file mode 100755 index 0000000..87b193e --- /dev/null +++ b/website/backend/app/src/views/fragments/return_board.etlua @@ -0,0 +1,3 @@ + diff --git a/website/backend/app/src/views/fragments/return_thread.etlua b/website/backend/app/src/views/fragments/return_thread.etlua new file mode 100755 index 0000000..77e5193 --- /dev/null +++ b/website/backend/app/src/views/fragments/return_thread.etlua @@ -0,0 +1,3 @@ + diff --git a/website/backend/app/src/views/index.etlua b/website/backend/app/src/views/index.etlua new file mode 100755 index 0000000..a50ad8e --- /dev/null +++ b/website/backend/app/src/views/index.etlua @@ -0,0 +1,16 @@ +

<%= site_name %>

+
+

<%= i18n('index_boards') %>

+ <% for _, board in ipairs(boards) do %> +
+ /<%= board.name %>/ - <%= board.title %> + <% if board.subtext then %> + <%= board.subtext %> + <% end %> + <% if board.rules then %> +

<%= i18n('rules') %>

+ <%- markdown(board.rules) %> + <% end %> +
+ <% end %> +
diff --git a/website/backend/app/src/views/install.etlua b/website/backend/app/src/views/install.etlua new file mode 100755 index 0000000..a049b6d --- /dev/null +++ b/website/backend/app/src/views/install.etlua @@ -0,0 +1,132 @@ +

Install Lapis-chan

+ +

Welcome to Lapis-chan v1.0.0!

+ +
+
+

Administrator Account Information

+ + + + + + + +

First Board Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/website/backend/app/src/views/layout.etlua b/website/backend/app/src/views/layout.etlua new file mode 100755 index 0000000..3c8f594 --- /dev/null +++ b/website/backend/app/src/views/layout.etlua @@ -0,0 +1,30 @@ + + + + + + + <% if board then %> + + <% if board.draw then %> + + + <% end %> + <% end %> + + <%= page_title %> + + + <% if errors then render('views.fragments.error') end %> + <% render('views.fragments.list_boards') %> + + <% content_for('inner') %> + <% render('views.fragments.list_boards') %> + <% render('views.fragments.copyright') %> + + diff --git a/website/backend/app/src/views/page.etlua b/website/backend/app/src/views/page.etlua new file mode 100755 index 0000000..f4abf3a --- /dev/null +++ b/website/backend/app/src/views/page.etlua @@ -0,0 +1,2 @@ +

<%= page.name %>

+<%- page.content %> diff --git a/website/backend/app/src/views/rules.etlua b/website/backend/app/src/views/rules.etlua new file mode 100755 index 0000000..4c9ccc3 --- /dev/null +++ b/website/backend/app/src/views/rules.etlua @@ -0,0 +1,17 @@ +

<%= i18n("rules") %>

+ + + + + + + + + <% for _, board in ipairs(boards) do %> + + + + + <% end %> + +
<%= i18n("board") %><%= i18n("rules") %>
<%= board.title %><%- board.rules %>
diff --git a/website/backend/app/src/views/thread.etlua b/website/backend/app/src/views/thread.etlua new file mode 100755 index 0000000..2230e92 --- /dev/null +++ b/website/backend/app/src/views/thread.etlua @@ -0,0 +1,26 @@ +<% render('views.fragments.board_title') %> +<% if not thread.lock or session.admin or session.mod then + render('views.fragments.form_submit') +end %> +<% render('views.fragments.announcements') %> +
+[<%= i18n('return') %>] +[<%= i18n('catalog') %>] +[<%= i18n('bottom') %>] +[<%= i18n('refresh') %>] +
+
+<% for i, post in ipairs(posts) do + if i == 1 then + render('views.fragments.op_content', { thread=thread, post=post }) + else + render('views.fragments.post_content', { thread=thread, post=post, op=posts[1] }) + end +end %> +
+
+[<%= i18n('return') %>] +[<%= i18n('catalog') %>] +[<%= i18n('top') %>] +[<%= i18n('refresh') %>] +
diff --git a/website/backend/app/static/banned.jpg b/website/backend/app/static/banned.jpg new file mode 100755 index 0000000000000000000000000000000000000000..16579efd71e5ae5b2af2db0b5c0adb051436fcc1 GIT binary patch literal 10148 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 0) + byteString = atob(dataURI.split(',')[1]); + else + byteString = unescape(dataURI.split(',')[1]); + + // separate out the mime component + var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + // write the bytes of the string to a typed array + var ia = new Uint8Array(byteString.length); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + + return new Blob([ia], {type:mimeString}); +} + +// Send quote text to submission form +// @param event A ClickEvent object +// @return none +function quote_post(event) { + var comment = document.getElementById("submit_comment"); + comment.value += ">>" + event.target.text + "\n"; + comment.focus(); +} + +// Send quote text to submission from when coming from board index +// @return none +function quote_thread() { + var hash = window.location.hash; + + if (hash[1] === "q") { + quote_post({ + target: { + text: hash.substring(2) + } + }); + } +} + +// Restrict the size of the comment block +// @return none +function restrict_comment_size() { + var comment = document.getElementById("submit_comment"); + + if (comment.value.length >= text_size) { + comment.value = comment.value.slice(0, text_size); + } +} + +// Cancel form submission if input is invalid +// @return valid +function validate_input() { + var file_input = document.getElementById("submit_file"); + + if (window.FileReader && file_input.files && file_input.files[0]) { + file = file_input.files[0]; + + if (file.size / 1024 / 1024 > body_size) { + alert("File size cannot exceed " + body_size + " MB."); + return false; + } + } +} + +// Toggle post menu visibility +// @return none +function toggle_menu() { + var child = this.parentNode.children[1]; + + if (child.style.display === "block") { + child.style.display = "none"; + } else { + child.style.display = "block"; + } +} + +// Add click events to all reply links +// @return none +function add_anchor_listeners() { + var elements = document.getElementsByClassName("link_reply"); + + for (var i=0; i < elements.length; i++) { + elements[i].onclick = quote_post; + } +} + +// Add change events to various form elements +// @return none +function add_change_listeners() { + var elements = document.getElementsByClassName("change_submit"); + + for (i=0; i < elements.length; i++) { + var element = elements[i]; + element.onchange = function(event) { + event.target.form.submit(); + }; + } + + var elements = document.getElementsByClassName("change_delete"); + + for (i=0; i < elements.length; i++) { + var element = elements[i]; + element.onchange = function(event) { + if (window.confirm("Are you sure you want to do this?")) { + event.target.form.submit(); + } + }; + } +} + +// Add click events to all menu buttons +// @return none +function add_menu_listeners() { + var buttons = document.getElementsByClassName("post_menu"); + + for (i=0; i < buttons.length; i++) { + var button = buttons[i]; + button.children[0].onclick = toggle_menu; + } +} + +// Add events to various user inputs +// @return none +function add_input_constraints() { + var comment = document.getElementById("submit_comment"); + if (comment) { + comment.onkeydown = restrict_comment_size; + comment.onkeyup = restrict_comment_size; + } + + var form = document.getElementById("submit_form"); + + if (form) { + form.onsubmit = validate_input; + } +} + +// Open a Tegaki canvas and add an image to it +// @param event A ClickEvent object +// @param path Path to image (optional) +function remix(event, path) { + var file = document.getElementById("draw"); + var clear = document.getElementById("tegaki_clear"); + + // get image + var image = new Image(); + image.src = path ? path : this.dataset.path; + + image.onload = function() { + Tegaki.open({ + onDone: function() { + var dataURL = Tegaki.flatten().toDataURL('image/png'); + //var blob = dataURItoBlob(dataURL); + + file.value = dataURL;//blob; + clear.disabled = false; + }, + onCancel: function() {}, + width: image.width, + height: image.height + }); + + // Force some data into Tegaki + Tegaki.layers[0].ctx.drawImage(image, 0, 0); + Tegaki.addLayer(); + Tegaki.setActiveLayer(Tegaki.layers.length); + } +} + +// Add ClickEvent listeners to Tegaki buttons +// @return none +function prepare_tegaki() { + var file = document.getElementById("draw"); + var width = document.getElementById("tegaki_width"); + var height = document.getElementById("tegaki_height"); + var draw = document.getElementById("tegaki_draw"); + var clear = document.getElementById("tegaki_clear"); + + // Standard draw button + if (draw) { + draw.onclick = function() { + Tegaki.open({ + onDone: function() { + var dataURL = Tegaki.flatten().toDataURL('image/png'); + //var blob = dataURItoBlob(dataURL); + + file.value = dataURL;//blob; + clear.disabled = false; + }, + onCancel: function() {}, + width: width.value > 2560 ? 2560 : width.value, + height: height.value > 2560 ? 2560 : height.value + }); + }; + } + + // Standard clear button + if (clear) { + clear.onclick = function() { + Tegaki.destroy(); + file.value = null; + clear.disabled = true; + }; + } + + // Remix buttons + var buttons = document.getElementsByClassName("remix"); + for (i=0; i < buttons.length; i++) { + var button = buttons[i]; + button.onclick = remix; + } + + // Come from board, auto-remix + var hash = window.location.hash; + if (hash[1] === "r") { + var button = document.getElementById(hash.slice(1)); + remix(null, button.dataset.path); + } +} + +// Check if document has loaded yet +// @return none +function ready() { + if (document.readyState != "loading") { + quote_thread(); + add_anchor_listeners(); + add_change_listeners() + add_menu_listeners(); + add_input_constraints(); + prepare_tegaki(); + } else { + document.addEventListener("DOMContentLoaded", quote_thread); + document.addEventListener("DOMContentLoaded", add_anchor_listeners); + document.addEventListener("DOMContentLoaded", add_change_listeners); + document.addEventListener("DOMContentLoaded", add_menu_listeners); + document.addEventListener("DOMContentLoaded", add_input_constraints); + document.addEventListener("DOMContentLoaded", prepare_tegaki); + } +} + +ready(); diff --git a/website/backend/app/static/js/tegaki/LICENSE b/website/backend/app/static/js/tegaki/LICENSE new file mode 100755 index 0000000..54d364f --- /dev/null +++ b/website/backend/app/static/js/tegaki/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Maxime Youdine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/website/backend/app/static/js/tegaki/README.md b/website/backend/app/static/js/tegaki/README.md new file mode 100755 index 0000000..6e3b4e7 --- /dev/null +++ b/website/backend/app/static/js/tegaki/README.md @@ -0,0 +1,10 @@ +[Demo](https://desuwa.github.io/tegaki.html) + +```javascript +Tegaki.open({ + onDone: function() { window.open(Tegaki.flatten().toDataURL('image/png')); }, + onCancel: function() { console.log('Closing...')}, + width: 380, + height: 380 +}); +``` diff --git a/website/backend/app/static/js/tegaki/Rakefile b/website/backend/app/static/js/tegaki/Rakefile new file mode 100755 index 0000000..1b88c69 --- /dev/null +++ b/website/backend/app/static/js/tegaki/Rakefile @@ -0,0 +1,20 @@ +Encoding.default_external = 'UTF-8' + +desc 'Run JShint' +task :jshint do |t| + require 'jshintrb' + + opts = { + laxbreak: true, + boss: true, + expr: true, + sub: true, + browser: true, + devel: true, + globalstrict: true, + unused: true, + '-W079' => true # no-native-reassign + } + + puts Jshintrb.report("'use strict';" + File.read('tegaki.js'), opts) +end diff --git a/website/backend/app/static/js/tegaki/tegaki.js b/website/backend/app/static/js/tegaki/tegaki.js new file mode 100755 index 0000000..775951d --- /dev/null +++ b/website/backend/app/static/js/tegaki/tegaki.js @@ -0,0 +1,1925 @@ +var TegakiBrush = { + brushFn: function(x, y) { + var i, ctx, dest, data, len, kernel; + + x = 0 | x; + y = 0 | y; + + ctx = Tegaki.ghostCtx; + dest = ctx.getImageData(x, y, this.brushSize, this.brushSize); + data = dest.data; + kernel = this.kernel; + len = kernel.length; + + i = 0; + while (i < len) { + data[i] = this.rgb[0]; ++i; + data[i] = this.rgb[1]; ++i; + data[i] = this.rgb[2]; ++i; + data[i] += kernel[i] * (1.0 - data[i] / 255); ++i; + } + + ctx.putImageData(dest, x, y); + }, + + commit: function() { + Tegaki.activeCtx.drawImage(Tegaki.ghostCanvas, 0, 0); + Tegaki.ghostCtx.clearRect(0, 0, + Tegaki.ghostCanvas.width, Tegaki.ghostCanvas.height + ); + }, + + draw: function(posX, posY, pt) { + var offset, mx, my, fromX, fromY, dx, dy, err, derr, step, stepAcc; + + offset = this.center; + step = this.stepSize; + stepAcc = this.stepAcc; + + if (pt === true) { + this.stepAcc = 0; + this.posX = posX; + this.posY = posY; + this.brushFn(posX - offset, posY - offset); + return; + } + + fromX = this.posX; + fromY = this.posY; + + if (fromX < posX) { dx = posX - fromX; mx = 1; } + else { dx = fromX - posX; mx = -1; } + if (fromY < posY) { dy = posY - fromY; my = 1; } + else { dy = fromY - posY; my = -1; } + + err = (dx > dy ? dx : -dy) / 2; + + dx = -dx; + + while (true) { + ++stepAcc; + if (stepAcc > step) { + this.brushFn(fromX - offset, fromY - offset); + stepAcc = 0; + } + if (fromX === posX && fromY === posY) { + break; + } + derr = err; + if (derr > dx) { err -= dy; fromX += mx; } + if (derr < dy) { err -= dx; fromY += my; } + } + + this.stepAcc = stepAcc; + this.posX = posX; + this.posY = posY; + }, + + generateBrush: function() { + var i, size, r, brush, ctx, dest, data, len, sqd, sqlen, hs, col, row, + ecol, erow, a; + + size = this.size * 2; + r = size / 2; + + brush = T$.el('canvas'); + brush.width = brush.height = size; + ctx = brush.getContext('2d'); + dest = ctx.getImageData(0, 0, size, size); + data = dest.data; + len = size * size * 4; + sqlen = Math.sqrt(r * r); + hs = Math.round(r); + col = row = -hs; + + i = 0; + while (i < len) { + if (col >= hs) { + col = -hs; + ++row; + continue; + } + + ecol = col; + erow = row; + + if (ecol < 0) { ecol = -ecol; } + if (erow < 0) { erow = -erow; } + + sqd = Math.sqrt(ecol * ecol + erow * erow); + + if (sqd > sqlen) { + a = 0; + } + else { + a = sqd / sqlen; + a = (Math.exp(1 - 1 / a) / a); + a = 255 - ((0 | (a * 100 + 0.5)) / 100) * 255; + } + + if (this.alphaDamp) { + a *= this.alpha * this.alphaDamp; + } + else { + a *= this.alpha; + } + + data[i + 3] = a; + + i += 4; + + ++col; + } + + ctx.putImageData(dest, 0, 0); + + this.center = r; + this.brushSize = size; + this.brush = brush; + this.kernel = data; + }, + + setSize: function(size, noBrush) { + this.size = size; + if (!noBrush) this.generateBrush(); + this.stepSize = Math.floor(this.size * this.step); + }, + + setAlpha: function(alpha, noBrush) { + this.alpha = alpha; + if (!noBrush) this.generateBrush(); + }, + + setColor: function(color, noBrush) { + this.rgb = Tegaki.hexToRgb(color); + if (!noBrush) this.generateBrush(); + }, + + set: function() { + this.setAlpha(this.alpha, true); + this.setSize(this.size, true); + this.setColor(Tegaki.toolColor, true); + this.generateBrush(); + } +}; + +var TegakiPen = { + init: function() { + this.size = 4; + this.alpha = 0.5; + this.step = 0.1; + this.stepAcc = 0; + }, + + draw: TegakiBrush.draw, + + commit: TegakiBrush.commit, + + brushFn: TegakiBrush.brushFn, + + generateBrush: function() { + var size, r, brush, ctx; + + size = this.size; + r = size / 2; + + brush = T$.el('canvas'); + brush.width = brush.height = size; + ctx = brush.getContext('2d'); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(r, r, r, 0, Tegaki.TWOPI, false); + ctx.fillStyle = '#000000'; + ctx.fill(); + ctx.closePath(); + + this.center = r; + this.brushSize = size; + this.brush = brush; + this.kernel = ctx.getImageData(0, 0, this.brushSize, this.brushSize).data; + }, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiPipette = { + size: 1, + alpha: 1, + noCursor: true, + + draw: function(posX, posY) { + var c, ctx; + + if (true) { + ctx = Tegaki.flatten().getContext('2d'); + } + else { + ctx = Tegaki.activeCtx; + } + + c = Tegaki.getColorAt(ctx, posX, posY); + + Tegaki.setToolColor(c); + Tegaki.updateUI('color'); + } +}; + +var TegakiAirbrush = { + init: function() { + this.size = 32; + this.alpha = 0.5; + this.alphaDamp = 0.2; + this.step = 0.25; + this.stepAcc = 0; + }, + + draw: TegakiBrush.draw, + + commit: TegakiBrush.commit, + + brushFn: TegakiBrush.brushFn, + + generateBrush: TegakiBrush.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiPencil = { + init: function() { + this.size = 1; + this.alpha = 1.0; + this.step = 0.25; + this.stepAcc = 0; + }, + + draw: TegakiBrush.draw, + + commit: TegakiBrush.commit, + + brushFn: function(x, y) { + var i, ctx, dest, data, len, kernel, a; + + x = 0 | x; + y = 0 | y; + + ctx = Tegaki.ghostCtx; + dest = ctx.getImageData(x, y, this.brushSize, this.brushSize); + data = dest.data; + kernel = this.kernel; + len = kernel.length; + + a = this.alpha * 255; + + i = 0; + while (i < len) { + data[i] = this.rgb[0]; ++i; + data[i] = this.rgb[1]; ++i; + data[i] = this.rgb[2]; ++i; + if (kernel[i] > 0) { + data[i] = a; + } + ++i; + } + + ctx.putImageData(dest, x, y); + }, + + generateBrush: TegakiPen.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiEraser = { + init: function() { + this.size = 8; + this.alpha = 1.0; + this.step = 0.25; + this.stepAcc = 0; + }, + + draw: TegakiBrush.draw, + + brushFn: function(x, y) { + var i, ctx, dest, data, len, kernel; + + x = 0 | x; + y = 0 | y; + + ctx = Tegaki.activeCtx; + dest = ctx.getImageData(x, y, this.brushSize, this.brushSize); + data = dest.data; + kernel = this.kernel; + len = kernel.length; + + for (i = 3; i < len; i += 4) { + if (kernel[i] > 0) { + data[i] = 0; + } + } + + ctx.putImageData(dest, x, y); + }, + + generateBrush: TegakiPen.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiDodge = { + init: function() { + this.size = 24; + this.alpha = 0.25; + this.alphaDamp = 0.05; + this.step = 0.25; + this.stepAcc = 0; + }, + + brushFn: function(x, y) { + var i, a, aa, ctx, dest, data, len, kernel; + + x = 0 | x; + y = 0 | y; + + ctx = Tegaki.activeCtx; + dest = ctx.getImageData(x, y, this.brushSize, this.brushSize); + data = dest.data; + kernel = this.kernel; + len = kernel.length; + + i = 0; + while (i < len) { + aa = kernel[i + 3] * 0.3; + a = 1 + kernel[i + 3] / 255; + data[i] = data[i] * a + aa; ++i; + data[i] = data[i] * a + aa; ++i; + data[i] = data[i] * a + aa; ++i; + ++i; + } + + ctx.putImageData(dest, x, y); + }, + + draw: TegakiBrush.draw, + + generateBrush: TegakiBrush.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiBurn = { + init: TegakiDodge.init, + + brushFn: function(x, y) { + var i, a, ctx, dest, data, len, kernel; + + x = 0 | x; + y = 0 | y; + + ctx = Tegaki.activeCtx; + dest = ctx.getImageData(x, y, this.brushSize, this.brushSize); + data = dest.data; + kernel = this.kernel; + len = kernel.length; + + i = 0; + while (i < len) { + a = 1 - kernel[i + 3] / 255; + data[i] = data[i] * a; ++i; + data[i] = data[i] * a; ++i; + data[i] = data[i] * a; ++i; + ++i; + } + + ctx.putImageData(dest, x, y); + }, + + draw: TegakiBrush.draw, + + generateBrush: TegakiDodge.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiBlur = { + init: TegakiDodge.init, + + brushFn: function(x, y) { + var i, j, ctx, src, size, srcData, dest, destData, lim, kernel, + sx, sy, r, g, b, a, aa, acc, kx, ky; + + x = 0 | x; + y = 0 | y; + + size = this.brushSize; + ctx = Tegaki.activeCtx; + src = ctx.getImageData(x, y, size, size); + srcData = src.data; + dest = ctx.createImageData(size, size); + destData = dest.data; + kernel = this.kernel; + lim = size - 1; + + for (sx = 0; sx < size; ++sx) { + for (sy = 0; sy < size; ++sy) { + r = g = b = a = acc = 0; + i = (sy * size + sx) * 4; + if (kernel[(sy * size + sx) * 4 + 3] === 0 + || sx === 0 || sy === 0 || sx === lim || sy === lim) { + destData[i] = srcData[i]; ++i; + destData[i] = srcData[i]; ++i; + destData[i] = srcData[i]; ++i; + destData[i] = srcData[i]; + continue; + } + for (kx = -1; kx < 2; ++kx) { + for (ky = -1; ky < 2; ++ky) { + j = ((sy - ky) * size + (sx - kx)) * 4; + aa = srcData[j + 3]; + acc += aa; + r += srcData[j] * aa; ++j; + g += srcData[j] * aa; ++j; + b += srcData[j] * aa; ++j; + a += srcData[j]; + } + } + destData[i] = r / acc; ++i; + destData[i] = g / acc; ++i; + destData[i] = b / acc; ++i; + destData[i] = a / 9; + } + } + + ctx.putImageData(dest, x, y); + }, + + draw: TegakiBrush.draw, + + generateBrush: TegakiDodge.generateBrush, + + setSize: TegakiBrush.setSize, + + setAlpha: TegakiBrush.setAlpha, + + setColor: TegakiBrush.setColor, + + set: TegakiBrush.set +}; + +var TegakiHistory = { + maxSize: 10, + + undoStack: [], + redoStack: [], + + pendingAction: null, + + clear: function() { + this.undoStack = []; + this.redoStack = []; + this.pendingAction = null; + }, + + push: function(action) { + this.undoStack.push(action); + + if (this.undoStack.length > this.maxSize) { + this.undoStack.shift(); + } + + if (this.redoStack.length > 0) { + this.redoStack = []; + } + }, + + undo: function() { + var action; + + if (!this.undoStack.length) { + return; + } + + action = this.undoStack.pop(); + action.undo(); + + this.redoStack.push(action); + }, + + redo: function() { + var action; + + if (!this.redoStack.length) { + return; + } + + action = this.redoStack.pop(); + action.redo(); + + this.undoStack.push(action); + } +}; + +var TegakiHistoryActions = { + Draw: function(layerId) { + this.canvasBefore = null; + this.canvasAfter = null; + this.layerId = layerId; + }, + + DestroyLayers: function(indices, layers) { + this.indices = indices; + this.layers = layers; + this.canvasBefore = null; + this.canvasAfter = null; + this.layerId = null; + }, + + AddLayer: function(layerId) { + this.layerId = layerId; + }, + + MoveLayer: function(layerId, up) { + this.layerId = layerId; + this.up = up; + } +}; + +TegakiHistoryActions.Draw.prototype.addCanvasState = function(canvas, type) { + if (type) { + this.canvasAfter = T$.copyCanvas(canvas); + } + else { + this.canvasBefore = T$.copyCanvas(canvas); + } +}; + +TegakiHistoryActions.Draw.prototype.exec = function(type) { + var i, layer; + + for (i in Tegaki.layers) { + layer = Tegaki.layers[i]; + + if (layer.id === this.layerId) { + layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + layer.ctx.drawImage(type ? this.canvasAfter: this.canvasBefore, 0, 0); + } + } +}; + +TegakiHistoryActions.Draw.prototype.undo = function() { + this.exec(0); +}; + +TegakiHistoryActions.Draw.prototype.redo = function() { + this.exec(1); +}; + +TegakiHistoryActions.DestroyLayers.prototype.undo = function() { + var i, ii, len, layers, idx, layer, frag; + + layers = new Array(len); + + for (i = 0; (idx = this.indices[i]) !== undefined; ++i) { + layers[idx] = this.layers[i]; + } + + i = ii = 0; + len = Tegaki.layers.length + this.layers.length; + frag = T$.frag(); + + while (i < len) { + if (!layers[i]) { + layer = layers[i] = Tegaki.layers[ii]; + Tegaki.layersCnt.removeChild(layer.canvas); + ++ii; + } + + if (this.layerId && layer.id === this.layerId) { + layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + layer.ctx.drawImage(this.canvasBefore, 0, 0); + } + + frag.appendChild(layers[i].canvas); + + ++i; + } + + Tegaki.layersCnt.insertBefore(frag, Tegaki.canvas.nextElementSibling); + + Tegaki.layers = layers; + + Tegaki.setActiveLayer(); + + Tegaki.rebuildLayerCtrl(); +}; + +TegakiHistoryActions.DestroyLayers.prototype.redo = function() { + var i, layer, ids = []; + + for (i = 0; layer = this.layers[i]; ++i) { + ids.push(layer.id); + } + + if (this.layerId) { + ids.push(this.layerId); + Tegaki.mergeLayers(ids); + } + else { + Tegaki.deleteLayers(ids); + } +}; + +TegakiHistoryActions.MoveLayer.prototype.undo = function() { + Tegaki.setActiveLayer(this.layerId); + Tegaki.moveLayer(this.layerId, !this.up); +}; + +TegakiHistoryActions.MoveLayer.prototype.redo = function() { + Tegaki.setActiveLayer(this.layerId); + Tegaki.moveLayer(this.layerId, this.up); +}; + +TegakiHistoryActions.AddLayer.prototype.undo = function() { + Tegaki.deleteLayers([this.layerId]); + Tegaki.layerIndex--; +}; + +TegakiHistoryActions.AddLayer.prototype.redo = function() { + Tegaki.addLayer(); + Tegaki.setActiveLayer(); +}; + +var T$ = { + docEl: document.documentElement, + + id: function(id) { + return document.getElementById(id); + }, + + cls: function(klass, root) { + return (root || document).getElementsByClassName(klass); + }, + + on: function(o, e, h) { + o.addEventListener(e, h, false); + }, + + off: function(o, e, h) { + o.removeEventListener(e, h, false); + }, + + el: function(name) { + return document.createElement(name); + }, + + frag: function() { + return document.createDocumentFragment(); + }, + + extend: function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } + }, + + selectedOptions: function(el) { + var i, opt, sel; + + if (el.selectedOptions) { + return el.selectedOptions; + } + + sel = []; + + for (i = 0; opt = el.options[i]; ++i) { + if (opt.selected) { + sel.push(opt); + } + } + + return sel; + }, + + copyCanvas: function(source) { + var canvas = T$.el('canvas'); + canvas.width = source.width; + canvas.height = source.height; + canvas.getContext('2d').drawImage(source, 0, 0); + + return canvas; + } +}; + +var TegakiStrings = { + // Messages + badDimensions: 'Invalid dimensions.', + promptWidth: 'Canvas width in pixels', + promptHeight: 'Canvas height in pixels', + confirmDelLayers: 'Delete selected layers?', + errorMergeOneLayer: 'You need to select at least 2 layers.', + confirmMergeLayers: 'Merge selected layers?', + errorLoadImage: 'Could not load the image.', + noActiveLayer: 'No active layer.', + hiddenActiveLayer: 'The active layer is not visible.', + confirmCancel: 'Are you sure? Your work will be lost.', + confirmChangeCanvas: 'Changing the canvas will clear all layers and history.', + + // UI + color: 'Color', + size: 'Size', + alpha: 'Opacity', + layers: 'Layers', + addLayer: 'Add layer', + delLayers: 'Delete layers', + mergeLayers: 'Merge layers', + showHideLayer: 'Toggle visibility', + moveLayerUp: 'Move up', + moveLayerDown: 'Move down', + tool: 'Tool', + changeCanvas: 'Change canvas', + blank: 'Blank', + newCanvas: 'New', + undo: 'Undo', + redo: 'Redo', + close: 'Close', + finish: 'Finish', + + // Tools + pen: 'Pen', + pencil: 'Pencil', + airbrush: 'Airbrush', + pipette: 'Pipette', + dodge: 'Dodge', + burn: 'Burn', + blur: 'Blur', + eraser: 'Eraser' +}; + +var Tegaki = { + VERSION: '0.0.1', + + bg: null, + cnt: null, + canvas: null, + ctx: null, + layers: [], + layersCnt: null, + ghostCanvas: null, + ghostCtx: null, + activeCtx: null, + activeLayer: null, + layerIndex: null, + + isPainting: false, + isErasing: false, + isColorPicking: false, + + offsetX: 0, + offsetY: 0, + + TWOPI: 2 * Math.PI, + + tools: { + pencil: TegakiPencil, + pen: TegakiPen, + airbrush: TegakiAirbrush, + pipette: TegakiPipette, + dodge: TegakiDodge, + burn: TegakiBurn, + blur: TegakiBlur, + eraser: TegakiEraser + }, + + tool: null, + toolColor: '#000000', + + bgColor: '#ffffff', + maxSize: 32, + maxLayers: 25, + baseWidth: null, + baseHeight: null, + + onDoneCb: null, + onCancelCb: null, + + open: function(opts) { + var bg, cnt, el, el2, tool, lbl, btn, ctrl, canvas, grp, self = Tegaki; + + if (self.bg) { + self.resume(); + return; + } + + if (opts.bgColor) { + self.bgColor = opts.bgColor; + } + + self.onDoneCb = opts.onDone; + self.onCancelCb = opts.onCancel; + + cnt = T$.el('div'); + cnt.id = 'tegaki-cnt'; + + canvas = T$.el('canvas'); + canvas.id = 'tegaki-canvas'; + canvas.width = self.baseWidth = opts.width; + canvas.height = self.baseHeight = opts.height; + + el = T$.el('div'); + el.id = 'tegaki-layers'; + el.appendChild(canvas); + self.layersCnt = el; + + cnt.appendChild(el); + + ctrl = T$.el('div'); + ctrl.id = 'tegaki-ctrl'; + + // Colorpicker + grp = T$.el('div'); + grp.className = 'tegaki-ctrlgrp'; + el = T$.el('input'); + el.id = 'tegaki-color'; + el.value = self.toolColor; + try { + el.type = 'color'; + } catch(e) { + el.type = 'text'; + } + lbl = T$.el('div'); + lbl.className = 'tegaki-label'; + lbl.textContent = TegakiStrings.color; + grp.appendChild(lbl); + T$.on(el, 'change', self.onColorChange); + grp.appendChild(el); + ctrl.appendChild(grp); + + // Size control + grp = T$.el('div'); + grp.className = 'tegaki-ctrlgrp'; + el = T$.el('input'); + el.id = 'tegaki-size'; + el.min = 1; + el.max = self.maxSize; + el.type = 'range'; + lbl = T$.el('div'); + lbl.className = 'tegaki-label'; + lbl.textContent = TegakiStrings.size; + grp.appendChild(lbl); + T$.on(el, 'change', self.onSizeChange); + grp.appendChild(el); + ctrl.appendChild(grp); + + // Alpha control + grp = T$.el('div'); + grp.className = 'tegaki-ctrlgrp'; + el = T$.el('input'); + el.id = 'tegaki-alpha'; + el.min = 0; + el.max = 1; + el.step = 0.01; + el.type = 'range'; + lbl = T$.el('div'); + lbl.className = 'tegaki-label'; + lbl.textContent = TegakiStrings.alpha; + grp.appendChild(lbl); + T$.on(el, 'change', self.onAlphaChange); + grp.appendChild(el); + ctrl.appendChild(grp); + + // Layer control + grp = T$.el('div'); + grp.className = 'tegaki-ctrlgrp'; + grp.id = 'tegaki-layer-grp'; + el = T$.el('select'); + el.id = 'tegaki-layer'; + el.multiple = true; + el.size = 3; + lbl = T$.el('div'); + lbl.className = 'tegaki-label'; + lbl.textContent = TegakiStrings.layers; + grp.appendChild(lbl); + T$.on(el, 'change', self.onLayerChange); + grp.appendChild(el); + el = T$.el('span'); + el.title = TegakiStrings.addLayer; + el.className = 'tegaki-icon tegaki-plus'; + T$.on(el, 'click', self.onLayerAdd); + grp.appendChild(el); + el = T$.el('span'); + el.title = TegakiStrings.delLayers; + el.className = 'tegaki-icon tegaki-minus'; + T$.on(el, 'click', self.onLayerDelete); + grp.appendChild(el); + el = T$.el('span'); + el.id = 'tegaki-layer-visibility'; + el.title = TegakiStrings.showHideLayer; + el.className = 'tegaki-icon tegaki-eye'; + T$.on(el, 'click', self.onLayerVisibilityChange); + grp.appendChild(el); + el = T$.el('span'); + el.id = 'tegaki-layer-merge'; + el.title = TegakiStrings.mergeLayers; + el.className = 'tegaki-icon tegaki-level-down'; + T$.on(el, 'click', self.onMergeLayers); + grp.appendChild(el); + el = T$.el('span'); + el.id = 'tegaki-layer-up'; + el.title = TegakiStrings.moveLayerUp; + el.setAttribute('data-up', '1'); + el.className = 'tegaki-icon tegaki-up-open'; + T$.on(el, 'click', self.onMoveLayer); + grp.appendChild(el); + el = T$.el('span'); + el.id = 'tegaki-layer-down'; + el.title = TegakiStrings.moveLayerDown; + el.className = 'tegaki-icon tegaki-down-open'; + T$.on(el, 'click', self.onMoveLayer); + grp.appendChild(el); + ctrl.appendChild(grp); + + // Tool selector + grp = T$.el('div'); + grp.className = 'tegaki-ctrlgrp'; + el = T$.el('select'); + el.id = 'tegaki-tool'; + for (tool in Tegaki.tools) { + el2 = T$.el('option'); + el2.value = tool; + el2.textContent = TegakiStrings[tool]; + el.appendChild(el2); + } + lbl = T$.el('div'); + lbl.className = 'tegaki-label'; + lbl.textContent = TegakiStrings.tool; + grp.appendChild(lbl); + T$.on(el, 'change', self.onToolChange); + grp.appendChild(el); + ctrl.appendChild(grp); + + cnt.appendChild(ctrl); + + el = T$.el('div'); + el.id = 'tegaki-menu-bar'; + + if (opts.canvasOptions) { + btn = T$.el('select'); + btn.id = 'tegaki-canvas-select'; + btn.title = TegakiStrings.changeCanvas; + btn.innerHTML = ''; + opts.canvasOptions(btn); + T$.on(btn, 'change', Tegaki.onCanvasSelected); + T$.on(btn, 'focus', Tegaki.onCanvasSelectFocused); + el.appendChild(btn); + } + + btn = T$.el('span'); + btn.className = 'tegaki-tb-btn'; + btn.textContent = TegakiStrings.newCanvas; + T$.on(btn, 'click', Tegaki.onNewClick); + el.appendChild(btn); + + btn = T$.el('span'); + btn.className = 'tegaki-tb-btn'; + btn.textContent = TegakiStrings.undo; + T$.on(btn, 'click', Tegaki.onUndoClick); + el.appendChild(btn); + + btn = T$.el('span'); + btn.className = 'tegaki-tb-btn'; + btn.textContent = TegakiStrings.redo; + T$.on(btn, 'click', Tegaki.onRedoClick); + el.appendChild(btn); + + btn = T$.el('span'); + btn.className = 'tegaki-tb-btn'; + btn.textContent = TegakiStrings.close; + T$.on(btn, 'click', Tegaki.onCancelClick); + el.appendChild(btn); + + btn = T$.el('span'); + btn.id = 'tegaki-finish-btn'; + btn.className = 'tegaki-tb-btn'; + btn.textContent = TegakiStrings.finish; + T$.on(btn, 'click', Tegaki.onDoneClick); + el.appendChild(btn); + + cnt.appendChild(el); + + bg = T$.el('div'); + bg.id = 'tegaki'; + self.bg = bg; + bg.appendChild(cnt); + document.body.appendChild(bg); + document.body.classList.add('tegaki-backdrop'); + + el = T$.el('canvas'); + el.id = 'tegaki-ghost-layer'; + el.width = canvas.width; + el.height = canvas.height; + self.ghostCanvas = el; + self.ghostCtx = el.getContext('2d'); + + self.cnt = cnt; + self.centerCnt(); + + self.canvas = canvas; + + self.ctx = canvas.getContext('2d'); + self.ctx.fillStyle = self.bgColor; + self.ctx.fillRect(0, 0, opts.width, opts.height); + + self.addLayer(); + + self.setActiveLayer(); + + self.initTools(); + + self.setTool('pencil'); + + self.updateUI(); + + self.updateCursor(); + self.updatePosOffset(); + + T$.on(self.bg, 'mousemove', self.onMouseMove); + T$.on(self.bg, 'mousedown', self.onMouseDown); + T$.on(self.layersCnt, 'contextmenu', self.onDummy); + + T$.on(document, 'mouseup', self.onMouseUp); + T$.on(window, 'resize', self.updatePosOffset); + T$.on(window, 'scroll', self.updatePosOffset); + }, + + initTools: function() { + var tool; + + for (tool in Tegaki.tools) { + (tool = Tegaki.tools[tool]) && tool.init && tool.init(); + } + }, + + hexToRgb: function(hex) { + var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i); + + if (c) { + return [ + parseInt(c[1], 16), + parseInt(c[2], 16), + parseInt(c[3], 16) + ]; + } + + return null; + }, + + centerCnt: function() { + var aabb, cnt; + + cnt = Tegaki.cnt; + aabb = cnt.getBoundingClientRect(); + + if (aabb.width > T$.docEl.clientWidth || aabb.height > T$.docEl.clientHeight) { + if (aabb.width > T$.docEl.clientWidth) { + cnt.classList.add('tegaki-overflow-x'); + } + if (aabb.height > T$.docEl.clientHeight) { + cnt.classList.add('tegaki-overflow-y'); + } + } + else { + cnt.classList.remove('tegaki-overflow-x'); + cnt.classList.remove('tegaki-overflow-y'); + } + + cnt.style.marginTop = -Math.round(aabb.height / 2) + 'px'; + cnt.style.marginLeft = -Math.round(aabb.width / 2) + 'px'; + }, + + getCursorPos: function(e, axis) { + if (axis === 0) { + return e.clientX + window.pageXOffset + Tegaki.bg.scrollLeft - Tegaki.offsetX; + } + else { + return e.clientY + window.pageYOffset + Tegaki.bg.scrollTop - Tegaki.offsetY; + } + }, + + resume: function() { + Tegaki.bg.classList.remove('tegaki-hidden'); + document.body.classList.add('tegaki-backdrop'); + + Tegaki.centerCnt(); + Tegaki.updatePosOffset(); + + T$.on(document, 'mouseup', Tegaki.onMouseUp); + T$.on(window, 'resize', Tegaki.updatePosOffset); + T$.on(window, 'scroll', Tegaki.updatePosOffset); + }, + + hide: function() { + Tegaki.bg.classList.add('tegaki-hidden'); + document.body.classList.remove('tegaki-backdrop'); + + T$.off(document, 'mouseup', Tegaki.onMouseUp); + T$.off(window, 'resize', Tegaki.updatePosOffset); + T$.off(window, 'scroll', Tegaki.updatePosOffset); + }, + + destroy: function() { + T$.off(Tegaki.bg, 'mousemove', Tegaki.onMouseMove); + T$.off(Tegaki.bg, 'mousedown', Tegaki.onMouseDown); + T$.off(Tegaki.layersCnt, 'contextmenu', Tegaki.onDummy); + + T$.off(document, 'mouseup', Tegaki.onMouseUp); + T$.off(window, 'resize', Tegaki.updatePosOffset); + T$.off(window, 'scroll', Tegaki.updatePosOffset); + + Tegaki.bg.parentNode.removeChild(Tegaki.bg); + + TegakiHistory.clear(); + + document.body.classList.remove('tegaki-backdrop'); + + Tegaki.bg = null; + Tegaki.cnt = null; + Tegaki.canvas = null; + Tegaki.ctx = null; + Tegaki.layers = []; + Tegaki.layerIndex = 0; + Tegaki.activeCtx = null; + }, + + flatten: function() { + var i, layer, canvas, ctx; + + canvas = T$.el('canvas'); + canvas.width = Tegaki.canvas.width; + canvas.height = Tegaki.canvas.height; + + ctx = canvas.getContext('2d'); + + ctx.drawImage(Tegaki.canvas, 0, 0); + + for (i = 0; layer = Tegaki.layers[i]; ++i) { + if (layer.canvas.classList.contains('tegaki-hidden')) { + continue; + } + ctx.drawImage(layer.canvas, 0, 0); + } + + return canvas; + }, + + updateUI: function(type) { + var i, ary, el, tool = Tegaki.tool; + + ary = type ? [type] : ['size', 'alpha', 'color']; + + for (i = 0; type = ary[i]; ++i) { + el = T$.id('tegaki-' + type); + el.value = type === 'color' ? Tegaki.toolColor : tool[type]; + + if (el.type === 'range') { + el.previousElementSibling.setAttribute('data-value', tool[type]); + } + } + }, + + rebuildLayerCtrl: function() { + var i, layer, sel, opt; + + sel = T$.id('tegaki-layer'); + + sel.textContent = ''; + + for (i = Tegaki.layers.length - 1; layer = Tegaki.layers[i]; i--) { + opt = T$.el('option'); + opt.value = layer.id; + opt.textContent = layer.name; + sel.appendChild(opt); + } + }, + + getColorAt: function(ctx, posX, posY) { + var rgba = ctx.getImageData(posX, posY, 1, 1).data; + + return '#' + + ('0' + rgba[0].toString(16)).slice(-2) + + ('0' + rgba[1].toString(16)).slice(-2) + + ('0' + rgba[2].toString(16)).slice(-2); + }, + + renderCircle: function(r) { + var i, canvas, ctx, d, e, x, y, dx, dy, idata, data, c, color; + + e = 1 - r; + dx = 0; + dy = -2 * r; + x = 0; + y = r; + d = 33; + c = 16; + + canvas = T$.el('canvas'); + canvas.width = canvas.height = d; + ctx = canvas.getContext('2d'); + idata = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + data = idata.data; + + color = 255; + + data[(c + (c + r) * d) * 4 + 3] = color; + data[(c + (c - r) * d) * 4 + 3] = color; + data[(c + r + c * d) * 4 + 3] = color; + data[(c - r + c * d) * 4 + 3] = color; + + while (x < y) { + if (e >= 0) { + y--; + dy += 2; + e += dy; + } + + ++x; + dx += 2; + e += dx; + + data[(c + x + (c + y) * d) * 4 + 3] = color; + data[(c - x + (c + y) * d) * 4 + 3] = color; + data[(c + x + (c - y) * d) * 4 + 3] = color; + data[(c - x + (c - y) * d) * 4 + 3] = color; + data[(c + y + (c + x) * d) * 4 + 3] = color; + data[(c - y + (c + x) * d) * 4 + 3] = color; + data[(c + y + (c - x) * d) * 4 + 3] = color; + data[(c - y + (c - x) * d) * 4 + 3] = color; + } + + if (r > 0) { + for (i = 0; i < 3; ++i) { + data[(c + c * d) * 4 + i] = 127; + } + data[(c + c * d) * 4 + i] = color; + } + + ctx.putImageData(idata, 0, 0); + + return canvas; + }, + + setToolSize: function(size) { + Tegaki.tool.setSize && Tegaki.tool.setSize(size); + Tegaki.updateCursor(); + }, + + setToolAlpha: function(alpha) { + Tegaki.tool.setAlpha && Tegaki.tool.setAlpha(alpha); + }, + + setToolColor: function(color) { + Tegaki.toolColor = color; + Tegaki.tool.setColor && Tegaki.tool.setColor(color); + Tegaki.updateCursor(); + }, + + setTool: function(tool) { + tool = Tegaki.tools[tool]; + Tegaki.tool = tool; + tool.set && tool.set(); + }, + + debugDumpPixelData: function(canvas) { + var i, idata, data, len, out, el; + + idata = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height); + data = idata.data; + len = data.length; + + out = ''; + + for (i = 0; i < len; i += 4) { + out += data[i] + ' ' + data[i+1] + ' ' + data[i+2] + ' ' + data[i+3] + '%0a'; + } + + el = document.createElement('a'); + el.href = 'data:,' + out; + el.download = 'dump.txt'; + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + }, + + debugDrawColors: function(sat) { + var i, ctx, grad, a; + + Tegaki.resizeCanvas(360, 360); + + ctx = Tegaki.activeCtx; + a = ctx.globalAlpha; + ctx.globalAlpha = 1; + + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, 360, 360); + + for (i = 0; i < 360; ++i) { + if (sat) { + grad = ctx.createLinearGradient(0, 0, 10, 360); + grad.addColorStop(0, 'hsl(' + i + ', 0%, ' + '50%)'); + grad.addColorStop(1, 'hsl(' + i + ', 100%, ' + '50%)'); + ctx.strokeStyle = grad; + } + else { + ctx.strokeStyle = 'hsl(' + i + ', 100%, ' + '50%)'; + } + ctx.beginPath(); + ctx.moveTo(i, 0); + ctx.lineTo(i, 360); + ctx.stroke(); + ctx.closePath(); + } + + if (!sat) { + grad = ctx.createLinearGradient(0, 0, 10, 360); + grad.addColorStop(0, '#000000'); + grad.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, 360, 360); + } + + ctx.globalAlpha = a; + }, + + onNewClick: function() { + var width, height, tmp; + + width = prompt(TegakiStrings.promptWidth, Tegaki.canvas.width); + if (!width) { return; } + + height = prompt(TegakiStrings.promptHeight, Tegaki.canvas.height); + if (!height) { return; } + + width = +width; + height = +height; + + if (width < 1 || height < 1) { + alert(TegakiStrings.badDimensions); + return; + } + + tmp = {}; + Tegaki.copyContextState(Tegaki.activeCtx, tmp); + Tegaki.resizeCanvas(width, height); + Tegaki.copyContextState(tmp, Tegaki.activeCtx); + + TegakiHistory.clear(); + Tegaki.centerCnt(); + Tegaki.updatePosOffset(); + }, + + onUndoClick: function() { + TegakiHistory.undo(); + }, + + onRedoClick: function() { + TegakiHistory.redo(); + }, + + onDoneClick: function() { + Tegaki.hide(); + Tegaki.onDoneCb(); + }, + + onCancelClick: function() { + if (!confirm(TegakiStrings.confirmCancel)) { + return; + } + + Tegaki.destroy(); + Tegaki.onCancelCb(); + }, + + onColorChange: function() { + Tegaki.setToolColor(this.value); + }, + + onSizeChange: function() { + this.previousElementSibling.setAttribute('data-value', this.value); + Tegaki.setToolSize(+this.value); + }, + + onAlphaChange: function() { + this.previousElementSibling.setAttribute('data-value', this.value); + Tegaki.setToolAlpha(+this.value); + }, + + onLayerChange: function() { + var selectedOptions = T$.selectedOptions(this); + + if (selectedOptions.length > 1) { + Tegaki.activeLayer = null; + } + else { + Tegaki.setActiveLayer(+this.value); + } + }, + + onLayerAdd: function() { + TegakiHistory.push(Tegaki.addLayer()); + Tegaki.setActiveLayer(); + }, + + onLayerDelete: function() { + var i, ary, sel, opt, selectedOptions, action; + + sel = T$.id('tegaki-layer'); + + selectedOptions = T$.selectedOptions(sel); + + if (Tegaki.layers.length === selectedOptions.length) { + return; + } + + if (!confirm(TegakiStrings.confirmDelLayers)) { + return; + } + + if (selectedOptions.length > 1) { + ary = []; + + for (i = 0; opt = selectedOptions[i]; ++i) { + ary.push(+opt.value); + } + } + else { + ary = [+sel.value]; + } + + action = Tegaki.deleteLayers(ary); + + TegakiHistory.push(action); + }, + + onLayerVisibilityChange: function() { + var i, ary, sel, opt, flag, selectedOptions; + + sel = T$.id('tegaki-layer'); + + selectedOptions = T$.selectedOptions(sel); + + if (selectedOptions.length > 1) { + ary = []; + + for (i = 0; opt = selectedOptions[i]; ++i) { + ary.push(+opt.value); + } + } + else { + ary = [+sel.value]; + } + + flag = !Tegaki.getLayerById(ary[0]).visible; + + Tegaki.setLayerVisibility(ary, flag); + }, + + onMergeLayers: function() { + var i, ary, sel, opt, selectedOptions, action; + + sel = T$.id('tegaki-layer'); + + selectedOptions = T$.selectedOptions(sel); + + if (selectedOptions.length > 1) { + ary = []; + + for (i = 0; opt = selectedOptions[i]; ++i) { + ary.push(+opt.value); + } + } + else { + ary = [+sel.value]; + } + + if (ary.length < 2) { + alert(TegakiStrings.errorMergeOneLayer); + return; + } + + if (!confirm(TegakiStrings.confirmMergeLayers)) { + return; + } + + action = Tegaki.mergeLayers(ary); + + TegakiHistory.push(action); + }, + + onMoveLayer: function(e) { + var id, action, sel; + + sel = T$.id('tegaki-layer'); + + id = +sel.options[sel.selectedIndex].value; + + if (action = Tegaki.moveLayer(id, e.target.hasAttribute('data-up'))) { + TegakiHistory.push(action); + } + }, + + onToolChange: function() { + Tegaki.setTool(this.value); + Tegaki.updateUI(); + Tegaki.updateCursor(); + }, + + onCanvasSelected: function() { + var img; + + if (!confirm(TegakiStrings.confirmChangeCanvas)) { + this.selectedIndex = +this.getAttribute('data-current'); + return; + } + + if (this.value === '0') { + Tegaki.ctx.fillStyle = Tegaki.bgColor; + Tegaki.ctx.fillRect(0, 0, Tegaki.baseWidth, Tegaki.baseHeight); + } + else { + img = T$.el('img'); + img.onload = Tegaki.onImageLoaded; + img.onerror = Tegaki.onImageError; + this.disabled = true; + img.src = this.value; + } + }, + + onImageLoaded: function() { + var el, tmp = {}; + + el = T$.id('tegaki-canvas-select'); + el.setAttribute('data-current', el.selectedIndex); + el.disabled = false; + + Tegaki.copyContextState(Tegaki.activeCtx, tmp); + Tegaki.resizeCanvas(this.naturalWidth, this.naturalHeight); + Tegaki.activeCtx.drawImage(this, 0, 0); + Tegaki.copyContextState(tmp, Tegaki.activeCtx); + + TegakiHistory.clear(); + Tegaki.centerCnt(); + Tegaki.updatePosOffset(); + }, + + onImageError: function() { + var el; + + el = T$.id('tegaki-canvas-select'); + el.selectedIndex = +el.getAttribute('data-current'); + el.disabled = false; + + alert(TegakiStrings.errorLoadImage); + }, + + resizeCanvas: function(width, height) { + var i, layer; + + Tegaki.canvas.width = width; + Tegaki.canvas.height = height; + Tegaki.ghostCanvas.width = width; + Tegaki.ghostCanvas.height = height; + + Tegaki.ctx.fillStyle = Tegaki.bgColor; + Tegaki.ctx.fillRect(0, 0, width, height); + + for (i = 0; layer = Tegaki.layers[i]; ++i) { + Tegaki.layersCnt.removeChild(layer.canvas); + } + + Tegaki.activeCtx = null; + Tegaki.layers = []; + Tegaki.layerIndex = 0; + T$.id('tegaki-layer').textContent = ''; + + Tegaki.addLayer(); + Tegaki.setActiveLayer(); + }, + + getLayerIndex: function(id) { + var i, layer, layers = Tegaki.layers; + + for (i = 0; layer = layers[i]; ++i) { + if (layer.id === id) { + return i; + } + } + + return -1; + }, + + getLayerById: function(id) { + return Tegaki.layers[Tegaki.getLayerIndex(id)]; + }, + + addLayer: function() { + var id, cnt, opt, canvas, layer, nodes, last; + + canvas = T$.el('canvas'); + canvas.className = 'tegaki-layer'; + canvas.width = Tegaki.canvas.width; + canvas.height = Tegaki.canvas.height; + + id = ++Tegaki.layerIndex; + + layer = { + id: id, + name: 'Layer ' + id, + canvas: canvas, + ctx: canvas.getContext('2d'), + visible: true, + empty: true, + opacity: 1.0 + }; + + Tegaki.layers.push(layer); + + cnt = T$.id('tegaki-layer'); + opt = T$.el('option'); + opt.value = layer.id; + opt.textContent = layer.name; + cnt.insertBefore(opt, cnt.firstElementChild); + + nodes = T$.cls('tegaki-layer', Tegaki.layersCnt); + + if (nodes.length) { + last = nodes[nodes.length - 1]; + } + else { + last = Tegaki.canvas; + } + + Tegaki.layersCnt.insertBefore(canvas, last.nextElementSibling); + + return new TegakiHistoryActions.AddLayer(id); + }, + + deleteLayers: function(ids) { + var i, id, len, sel, idx, indices, layers; + + sel = T$.id('tegaki-layer'); + + indices = []; + layers = []; + + for (i = 0, len = ids.length; i < len; ++i) { + id = ids[i]; + idx = Tegaki.getLayerIndex(id); + sel.removeChild(sel.options[Tegaki.layers.length - 1 - idx]); + Tegaki.layersCnt.removeChild(Tegaki.layers[idx].canvas); + + indices.push(idx); + layers.push(Tegaki.layers[idx]); + + Tegaki.layers.splice(idx, 1); + } + + Tegaki.setActiveLayer(); + + return new TegakiHistoryActions.DestroyLayers(indices, layers); + }, + + mergeLayers: function(ids) { + var i, id, sel, idx, canvasBefore, destId, dest, action; + + sel = T$.id('tegaki-layer'); + + destId = ids.pop(); + idx = Tegaki.getLayerIndex(destId); + dest = Tegaki.layers[idx].ctx; + + canvasBefore = T$.copyCanvas(Tegaki.layers[idx].canvas); + + for (i = ids.length - 1; i >= 0; i--) { + id = ids[i]; + idx = Tegaki.getLayerIndex(id); + dest.drawImage(Tegaki.layers[idx].canvas, 0, 0); + } + + action = Tegaki.deleteLayers(ids); + action.layerId = destId; + action.canvasBefore = canvasBefore; + action.canvasAfter = T$.copyCanvas(dest.canvas); + + Tegaki.setActiveLayer(destId); + + return action; + }, + + moveLayer: function(id, up) { + var idx, sel, opt, canvas, tmp, tmpId; + + sel = T$.id('tegaki-layer'); + idx = Tegaki.getLayerIndex(id); + + canvas = Tegaki.layers[idx].canvas; + opt = sel.options[Tegaki.layers.length - 1 - idx]; + + if (up) { + if (!Tegaki.ghostCanvas.nextElementSibling) { return false; } + canvas.parentNode.insertBefore(canvas, + Tegaki.ghostCanvas.nextElementSibling.nextElementSibling + ); + opt.parentNode.insertBefore(opt, opt.previousElementSibling); + tmpId = idx + 1; + } + else { + if (canvas.previousElementSibling.id === 'tegaki-canvas') { return false; } + canvas.parentNode.insertBefore(canvas, canvas.previousElementSibling); + opt.parentNode.insertBefore(opt, opt.nextElementSibling.nextElementSibling); + tmpId = idx - 1; + } + + Tegaki.updateGhostLayerPos(); + + tmp = Tegaki.layers[tmpId]; + Tegaki.layers[tmpId] = Tegaki.layers[idx]; + Tegaki.layers[idx] = tmp; + + Tegaki.activeLayer = tmpId; + + return new TegakiHistoryActions.MoveLayer(id, up); + }, + + setLayerVisibility: function(ids, flag) { + var i, len, sel, idx, layer, optIdx; + + sel = T$.id('tegaki-layer'); + optIdx = Tegaki.layers.length - 1; + + for (i = 0, len = ids.length; i < len; ++i) { + idx = Tegaki.getLayerIndex(ids[i]); + layer = Tegaki.layers[idx]; + layer.visible = flag; + + if (flag) { + sel.options[optIdx - idx].classList.remove('tegaki-strike'); + layer.canvas.classList.remove('tegaki-hidden'); + } + else { + sel.options[optIdx - idx].classList.add('tegaki-strike'); + layer.canvas.classList.add('tegaki-hidden'); + } + } + }, + + setActiveLayer: function(id) { + var ctx, idx; + + idx = id ? Tegaki.getLayerIndex(id) : Tegaki.layers.length - 1; + + if (idx < 0 || idx > Tegaki.maxLayers) { + return; + } + + ctx = Tegaki.layers[idx].ctx; + + if (Tegaki.activeCtx) { + Tegaki.copyContextState(Tegaki.activeCtx, ctx); + } + + Tegaki.activeCtx = ctx; + Tegaki.activeLayer = idx; + T$.id('tegaki-layer').selectedIndex = Tegaki.layers.length - idx - 1; + + Tegaki.updateGhostLayerPos(); + }, + + updateGhostLayerPos: function() { + Tegaki.layersCnt.insertBefore( + Tegaki.ghostCanvas, + Tegaki.activeCtx.canvas.nextElementSibling + ); + }, + + copyContextState: function(src, dest) { + var i, p, props = [ + 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha', + 'lineWidth', 'globalCompositeOperation' + ]; + + for (i = 0; p = props[i]; ++i) { + dest[p] = src[p]; + } + }, + + updateCursor: function() { + var radius; + + radius = 0 | (Tegaki.tool.size / 2); + + if (Tegaki.tool.noCursor || radius < 1) { + Tegaki.layersCnt.style.cursor = 'default'; + return; + } + + Tegaki.layersCnt.style.cursor = 'url("' + + Tegaki.renderCircle(radius).toDataURL('image/png') + + '") 16 16, default'; + }, + + updatePosOffset: function() { + var aabb = Tegaki.canvas.getBoundingClientRect(); + Tegaki.offsetX = aabb.left + window.pageXOffset + Tegaki.cnt.scrollLeft; + Tegaki.offsetY = aabb.top + window.pageYOffset + Tegaki.cnt.scrollTop; + }, + + onMouseMove: function(e) { + if (Tegaki.isPainting) { + Tegaki.tool.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1)); + } + else if (Tegaki.isColorPicking) { + TegakiPipette.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1)); + } + }, + + onMouseDown: function(e) { + if (e.target.parentNode === Tegaki.layersCnt) { + if (Tegaki.activeLayer === null) { + alert(TegakiStrings.noActiveLayer); + return; + } + if (!Tegaki.layers[Tegaki.activeLayer].visible) { + alert(TegakiStrings.hiddenActiveLayer); + return; + } + } + else if (e.target !== Tegaki.bg) { + return; + } + + if (e.which === 3 || e.altKey) { + Tegaki.isColorPicking = true; + TegakiPipette.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1)); + } + else { + Tegaki.isPainting = true; + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.layers[Tegaki.activeLayer].id + ); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeCtx.canvas, 0); + Tegaki.tool.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1), true); + } + }, + + onMouseUp: function(e) { + if (Tegaki.isPainting) { + Tegaki.tool.commit && Tegaki.tool.commit(); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeCtx.canvas, 1); + TegakiHistory.push(TegakiHistory.pendingAction); + Tegaki.isPainting = false; + } + else if (Tegaki.isColorPicking) { + e.preventDefault(); + Tegaki.isColorPicking = false; + } + }, + + onDummy: function(e) { + e.preventDefault(); + e.stopPropagation(); + } +}; diff --git a/website/backend/app/static/op_audio.png b/website/backend/app/static/op_audio.png new file mode 100755 index 0000000000000000000000000000000000000000..e6f3f5995da20f22da7784673453c41a4f37e68b GIT binary patch literal 19745 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 BFuVW& literal 0 HcmV?d00001 diff --git a/website/backend/app/static/post_spoiler.png b/website/backend/app/static/post_spoiler.png new file mode 100755 index 0000000000000000000000000000000000000000..3779307f9d05a6734ee7fbf42a7f7ad994b55310 GIT binary patch literal 18948 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 literal 0 HcmV?d00001 diff --git a/website/backend/app/wait-for-it.sh b/website/backend/app/wait-for-it.sh new file mode 100755 index 0000000..bbe4043 --- /dev/null +++ b/website/backend/app/wait-for-it.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +cmdname=$(basename $0) + +echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $TIMEOUT -gt 0 ]]; then + echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" + else + echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" + fi + start_ts=$(date +%s) + while : + do + if [[ $ISBUSY -eq 1 ]]; then + nc -z $HOST $PORT + result=$? + else + (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 + result=$? + fi + if [[ $result -eq 0 ]]; then + end_ts=$(date +%s) + echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" + break + fi + sleep 1 + done + return $result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $QUIET -eq 1 ]]; then + timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + else + timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + fi + PID=$! + trap "kill -INT -$PID" INT + wait $PID + RESULT=$? + if [[ $RESULT -ne 0 ]]; then + echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" + fi + return $RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + hostport=(${1//:/ }) + HOST=${hostport[0]} + PORT=${hostport[1]} + shift 1 + ;; + --child) + CHILD=1 + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -s | --strict) + STRICT=1 + shift 1 + ;; + -h) + HOST="$2" + if [[ $HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + HOST="${1#*=}" + shift 1 + ;; + -p) + PORT="$2" + if [[ $PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + PORT="${1#*=}" + shift 1 + ;; + -t) + TIMEOUT="$2" + if [[ $TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$HOST" == "" || "$PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +TIMEOUT=${TIMEOUT:-15} +STRICT=${STRICT:-0} +CHILD=${CHILD:-0} +QUIET=${QUIET:-0} + +# check to see if timeout is from busybox? +# check to see if timeout is from busybox? +TIMEOUT_PATH=$(realpath $(which timeout)) +if [[ $TIMEOUT_PATH =~ "busybox" ]]; then + ISBUSY=1 + BUSYTIMEFLAG="-t" +else + ISBUSY=0 + BUSYTIMEFLAG="" +fi + +if [[ $CHILD -gt 0 ]]; then + wait_for + RESULT=$? + exit $RESULT +else + if [[ $TIMEOUT -gt 0 ]]; then + wait_for_wrapper + RESULT=$? + else + wait_for + RESULT=$? + fi +fi + +if [[ $CLI != "" ]]; then + if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then + echoerr "$cmdname: strict mode, refusing to execute subprocess" + exit $RESULT + fi + exec "${CLI[@]}" +else + exit $RESULT +fi diff --git a/website/backend/data/backup/.keep b/website/backend/data/backup/.keep new file mode 100755 index 0000000..e69de29 diff --git a/website/backend/data/favicon.ico b/website/backend/data/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..dc9fcd34dc7ac221fbdf6cd9268ce09d52a24e70 GIT binary patch literal 954 zcmV;r14aCaP)~ZgPy1J zaB^PG`4Kpw*u9w%nqhAUUe^T6QX&8tx>=M<)w!15`sSH*dG+6U;vaKWdd#Z4>wf5T z=Oo9m#9^hTsdX(&5CHGsJKgP0URMn(wUwR9wR>;AyngoRRl!y1G20TnKSc&Z=l8ZU z+tqUYRxuEm_vi8=XmEL-KPwjUF?YN3tDfGD-rbG#uVm%my(d!KAOS$*g@@t6uJh~5 ziSG|eRunX%QPwO)MtgFR;hsc)Q`xaBqLSA4qif5FneagHf@*U;003fKnv3?0bT1VP z<&C|P;7(v&@XIw&T$vU(NroXl z;cYA-muq*P-Mp-OyuznGzXxl7?#@n6p6lKyCqg^x$wKc?7e`af-395QCRggWZ4Mg& z1~*8-=lP&gsy2i{^*I1=EZfm+nou=u5deO_6IWIBXoF^GI6cm{0KhQqKOPpVipT4o zAUTfpYuZujNjz=>KtoZsDTap2-4?&}NEe%$s;5|<3D75h za9FBI$<546^lblVE_qO-84CWO_jq%fdX=|`;{{1W*YiBjbiX70Oas9D=M%GAo2d&+ zi)+6q4K-6K)e?&fOKHoppf&U)0K75vg5i0#+t8Z(WaU7f^N21xL)FIt;N34~gLKQ% zpWeRo_Vl%LUB&(KM;#%bUukHN<=Qj=h=qJi5Z!#GD9;kHdov@wL*eCe;c&BR)<&(v z*882|z;|ZTltB=tTWIr$eh<0wa(yjr6vnu!&kFeI6+v`Gc2<)k2q z4A2_$s6{YErJ-eP4o+~o>|&#)9R2az(xY;<{3Zaj*_+>XMFM^4?fv`n(!?4K09m(q zi(X9)4@A4Kq!anqDtY<(K}mbTSvsHh`Ll3|5DU#%i1U6OI^&OQuIw$;b@uN6!W8W7 zm~=R7gj}v_N=?lY1YmR6It9_iH|naD*~$GErjs~RH1)CI6|S%>-2niG(ag$4`3K7& crcUDgH+hGZ7Xz*~6aWAK07*qoM6N<$f`}^DNB{r; literal 0 HcmV?d00001 diff --git a/website/backend/data/files/.keep b/website/backend/data/files/.keep new file mode 100755 index 0000000..e69de29 diff --git a/website/backend/data/robots.txt b/website/backend/data/robots.txt new file mode 100755 index 0000000..eb05362 --- /dev/null +++ b/website/backend/data/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/website/backend/data/secrets/.keep b/website/backend/data/secrets/.keep new file mode 100755 index 0000000..e69de29 diff --git a/website/backend/dev.sh b/website/backend/dev.sh new file mode 100755 index 0000000..5691d4c --- /dev/null +++ b/website/backend/dev.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker-compose --env-file ./.env.dev up diff --git a/website/backend/docker-compose.yml b/website/backend/docker-compose.yml new file mode 100755 index 0000000..e5915ca --- /dev/null +++ b/website/backend/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3' + +services: + # PostgreSQL database server + psql: + build: + context: ./psql + dockerfile: Dockerfile + environment: + - PGDATA:/var/lib/postgresql/data/pgdata + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - postgres:/var/lib/postgresql/data + - ./psql/pg_hba.conf:/var/lib/postgres/data/pg_hba.conf + - ./data/backup:/pgbackup + restart: unless-stopped + + # Lapis-chan + app: + build: + context: ./app + dockerfile: Dockerfile + depends_on: + - psql + volumes: + - ./app:/var/www + - ./data:/var/data + ports: + - 1001:80 + restart: unless-stopped + command: bash -c "/usr/local/bin/docker-entrypoint.sh ${LAPIS_CONFIG}" + +volumes: + postgres: diff --git a/website/backend/prod.sh b/website/backend/prod.sh new file mode 100755 index 0000000..b408add --- /dev/null +++ b/website/backend/prod.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker-compose --env-file ./.env.prod up diff --git a/website/backend/psql/1-init.sql b/website/backend/psql/1-init.sql new file mode 100755 index 0000000..98ffb88 --- /dev/null +++ b/website/backend/psql/1-init.sql @@ -0,0 +1,5 @@ +CREATE DATABASE lapischan; +ALTER DATABASE lapischan OWNER TO postgres; + +CREATE DATABASE lapischan_test; +ALTER DATABASE lapischan_test OWNER TO postgres; diff --git a/website/backend/psql/Dockerfile b/website/backend/psql/Dockerfile new file mode 100755 index 0000000..8960055 --- /dev/null +++ b/website/backend/psql/Dockerfile @@ -0,0 +1,2 @@ +FROM postgres:latest +ADD 1-init.sql /docker-entrypoint-initdb.d/1-init.sql diff --git a/website/backend/psql/pg_hba.conf b/website/backend/psql/pg_hba.conf new file mode 100755 index 0000000..e62e587 --- /dev/null +++ b/website/backend/psql/pg_hba.conf @@ -0,0 +1,15 @@ +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 0.0.0.0/0 trust +# IPv6 local connections: +host all all ::0/0 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +#local replication postgres trust +#host replication postgres 127.0.0.1/32 trust +#host replication postgres ::1/128 trust + +host all all 0.0.0.0/0 md5 diff --git a/website/backend/resources/op_spoiler.psd b/website/backend/resources/op_spoiler.psd new file mode 100755 index 0000000000000000000000000000000000000000..cdc656e986f2c32b596291962c87cb30b02ed2f9 GIT binary patch literal 255845 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 z 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%0APTUSclfW8E2te&<*r~sBkoJ50-86M-g^OUP z_-5J?lgC?F7@f2*td7LV5QZJU6OB^VL&s(O6y;0m#%K|{Vg^bE zw(KLw-D-`ahN-!^-57Wi+^G*qEm;OhW?^K5@K;3AmgKQt@^sA(_k9xe<0C8kEN%39 z*eObm(9s;GP#q-LDZZD;N?1jND5g)I)5j6z_o5b6=$`bvIqVeWEK&!Zy>x;Kd@!hA z52+tJ2!~*8NEUWeU0-u^uPwxqfv-cJLY~4C?50E)JC@~kK~&i|2dxlAasG|#_&x#G z<5vkoj%4B1YhkA<-H|zVblkklaHsU7mUTm)|rGt7%yfuu&+nx>8iPjP0yf+eO8T;2jPu-Hy$-kc=Pf_^* z7#(Gm1?vi`erW3=lI!TjK^dm1vu|wL>~JnFudFQ3+l;eOswP@+DByig zb;tdEG?Vz3%|JSnH-d1op=M9{Hnr2B>7`N8S%3q|W8kLtFr#T|NnZqp0_At+Iv8}m z4Mn}rU$Ti-Fm!JPHk{U}(Sfe^78S~Bq;JcW&Fwuydds3G{NQ-7saY7gG-bV=)qQy* zeM~som)?NW9FIQ~kNhy`b+djZhhuiuxwz`~h0sFgbyEq*QD{>*%<907l%{TM^P?sM zW#3ed2szmR0COOHlrVZ&Q(R~-oE8eAPY(7S1vSSYO#!$wlEsWBvhA zqdP&29fvf<_mFgD(v>ikym_e#2lXkNz=(Q5gw^02hcxBG+Q1C4x;&_Xx4D7LnWFO* zDy-~=Pw4?&vZ#~>0Bs$;=Nx+}VntejK#qk{JZC#=+l4xT{c5UiO z(-pS@oGDOwTP>3)dM4(vY=hoeuq5(^AG|5fl;ETz$8>dlM=)8F&1&31d=$;=JyF%W zO6uqA8nvu=!zdv2dtF#xUjI8-Oo3I)XopnNbdJ5{IGico(@oY#Vx!X4bWdy&MZ0*# zGLSj@gR1YN`?OJ|8J?b9+VHVr?@%zX?LUoPR;v6CdDl-Zm_~clXd6xDAzlMAWrK-8 zQLAh}Q1HpJ`sCK-ovc+iGU9)d8nPgPav>bib3agY@ag!^jI5H&y! zy$)eYr=mSuUT4{&hM!QV7p(C;__l~>cWJ8IsEVl4TXbuvzX7cb!fiSVUy2`MTZOc7 z0X6n8M4`S~v`nDN311u)1svKIs|uZI>QUzwB29MAd@Q`wUIUx+M%ii<8g>GM9E6H7 zYhd#vPHnl&gJ^S7!I!{7QmQoL3o%x}0ypREG4N8Hi;C{+Rjvr58ihj#%l zwe7NNd*MR>Z9ti>>K=1=<8K5owS>xCl;5{{$@Ebt+azkwY4E*X08B;J=8dC$?H-I? zGdLm4uZEb<`Wk?#X<}5;?x!Xgw2Z|pXc?nDM%z zrS{tu^n9l;jMkiF4<5Df3QEn=@#s>KX0(OWwR@>&3|bL2G&Z0$Lr_eFjscen;u*E1 z#TBJqLGR{bOD**ZqJ~#Fy>&dcl$SlJT8%|9O5YGVQ4x8yT6T{Imhw>7R)cJQ%L6V? zf=jCc-HCXz!@7Gsu#_v+L$>K>PkLe-19)dtCA3+$j|G-uuaqLIlWd3#MJ>uY1wGh2 z5j+l8$_qiHqg1@#AE72pZ_GW?P)|{yO0C_9Gf=5djsTSkwnK}kU$Av`)5F>@gHg1T zkcfbrplmk>7^sxr-2T%eK&4`&MQF;CS<#!iNiD?Dps^Y4$Psk52z`zqwThk!Ir^xP z?cC9zQvA*kYj~85p+y~Je48HD6CPBF1}r{QCje3^?z5m|7e@McNU2e(oLAI#tfE&l z22z38`j`Uke}oT3Xk$0#y$KjUeiG3@H(j9MTa^=ncaTU+kl!!KLivo1FsF(n){R1BtvvO;W2< z8fmIo{X1w@mvv3AQnPx}wLc78id&DdZfPJ|N8BhzKOKzv7jzv>bq%T3`Gz`ahi)Md z#n~r(&|`3=qC;3j4fTDCK+H(?^v~&g6cWkvo_-13NBtc2^lv3ey#y+SZ@)ET^Q7kA zrjgYcsu>ArAh2W{ZUYv;%RmG5(zapd&3Yq5?-Imb!#g?Ddchg0w5lM)nK0-{3cez55I}kFlk?c zj~w!O+(PKVE2vU}f3PK{I!#nZmm~kbKqeRh1VAPt%cIQ3ynvOmwM5xW%7$hEfAFTD z7^eY>FgplP3_4_f304YNn^ACz_9}aL=0Rq}X)X11Uf$Vc*ss z0x|>M-#3HbhkXq11kf6R<(WPO-oPAHyg?I3(JVug+Of3C@T5e`LrOxp0nA3d ztgUbCwFoz3eQiL?X#!Y|(5zi%ULwPj@=UdOl88Hqci#YUr-2Z6JSw4OBJcuAYI}Zg z;~#+hv!N0O=eD0iNuBT+Itlh@gGPA>(h%~IN_DGRWArhXk>I36?#WKIRW%Q9vTJ|_$A5M4!)LN?fEcZN z_dT5VJL|wJ>Xv==Vg6tJ@o)Z;=srQy#osq^C%^ZjKhLY_nBVpTLjL6Td`C^*pa1B4 zPq<;a?cmZ+|K{J^?+dO2J~}nbPWJ=l-|xa-V>pf}I^Bc6w7?I|>ct-r$moCU#~&DF?LQmD z9~jqG{t*5E%liLw1b_4-xZ7I%3nq;JZVZ2D65N$>{EK6}k$<7bA2^BI`F|$xM;kuu zIf;L9P^A5pX|6Ky?v87s`Pn`3`K-VE^FKLr=1>0o^j~J37vFoE)U;BT*Z~`?^oo42~NQj0D{tasBFf+oxEf|Ih>A!E`zDfvv z*%?lJ7(n>vIejSeSp1)?z*pm&|F>Cu>R9-fHY($A`=(Z%sw zcJ3Kno8#v2yN^RN3GVkCpodHIoP!g7Uc7-X-~FdqV6x$ole2Nc+qtVo{A!M?p-T&# zg%jQ?2$&MwZ_R^0Tw3Hz9RG`V0q3y)8&2*iU0UMsF|HrUED3(|e_p^+!=+^o{Z)VB zuqOER?=ND!xU|Bd&;7(*Tr%=Y+^2MDl|zl#znicnxV9y(nyzee@1FSOU;j^k^*;)4 zeDvUx>PPqPl&7Ej>mUE?Up|czt{eE_!)5Lh`r$rzBICz-wf#%T66`~lR)?PDp8e+l z@%;}A_5}CA3dufR{z09?gW~~_^{fB>zv3%8Z#UrHZ?9sGLkVHB;@{wV@)sRYnIEp< z5@5%57yb#rb${14o8YGDk~bmj*FhHiFOTEWqjg-uNmAz@q6?-G7yfJm7jSCT_3zPI za2p42@qgsP71V7!`^Nyx&BvM96kWg}T695 z{c?iKjr;rO!c{Y_{@WmZ#OD3|n+a=zD)#vw#&B?B3DRe1-rv6k zOTc}EpJw8xe-WZj$h^OQJAsnGzre*@jL;*5Ibbxuzkdhzfy=uv(7`+wl#E&Oux?I^BtWq0V}0^~J^ zW17+^z3Ei+4vY?0hF|_Vei=Ct!&NT(c49igF$v(8KV#Qi?3xsB=iN9hS&xBz?$2+* zJaPXDPygTe;eXkrYty%&a-0vW4&YT?`0W&Z7?=Cao478-A8Ik){~b^K6Fl*+x9CpY zO&r~e;_v_c7NkZG(KkT8-Xc-{T>K`EnsM)+X>spg;_dHjLt*yc+s0ik`v%T=ag&ol z_a%6+2oL^@-Ll-k$tT@X;}%B3r-yL{W7@{7aldgLU!-~UrQ_8;(A8E(1g7WaGCuo>Y_Kc1??Q}6EK z*6;4ptw-OABTX8&sHKWaqcO)MbzyO-abFMxLlhSjkbO}>WDymO z;({Q8;sQj$`%QoEU~KzO|GD4&o@Zw6y>n;Y_nG(JJ7fAxtg!20{ecD;av@oZOecvG zvJ~S`0<^%8N7-9!`pRBNtHqbBOTZG{(z(6lfma{XYretHsX?&)n2Ea1Q$edYV5>|= zi5E7Ml5bKi0fr2L+{t1>w)3r!(U7|!rxS+H@1_YabG>j%3|fNJ8hJudXjTjy>JJF8wIzUT6!Jo4^W>l z#f{B;Xi&4EW!5GBD!X04Ba^Ggat&kaWyZl(SeBnGC$oeXVr}QBa1Z_9K7(uSXex99 zqZ->uLQSadFu|N$pn8{W5U*g_PO=d1KnwsHrY4io{SqTn+er~-G|bOoY&8$&mKTF) z+c_r8WKP|?u3D?Vt5s}^=%KcgD$E>CyclSO+QBx7-efyzLVOO9476P(u-(O?Ha2rP zLPSEu0PR=lY$GVyb~1$601*Y0rU+}R#keZl$rK_Qf{RoI>MYw?xHa3!5+WXAHc*+m z%r@jZ2zRy+i4cDQx}>Vv2ICv&7(5w#B1n}&f~)s0C{>;#=~KbSfYd6!BvPZmM;JT< zd^E^yB`s$}xl+}+l0FA~1jyZ|_*8>uKgCTsOsDUv&yx)4PYSAQJ@9#@rW#yMNDh2U zRj~~|r_}Z1lKu$zM3Ac1D+7Pe;6>oA7iOx0ubdN4=v;0Yx30A&r0$j=Ykb`&Ay5}| zfY!=OgNfvY?};ZxWQrg%)tzc?P|O*n;`2pxiY!vQNxQH0mZFC|uNCMlVR-H;_jH?D z$+j37)3re4sc0lrhO~mYw47RRnBk|ik<~Q3HmxnkG`E*al$uhAlqC~m-B(O=8^(4( zF;4ZgXj`|r z*aO#HtWiBDT9Jn$TYpg>7Ls*0u40aEdy9e0Bg{B~a@>S!;t`mes2-Lv#6xQ+- zchuL=m3qI>IPYI$xo>$W{E0dYy@VRGof|w7gT@no26oUm1*xc#RAH<1x5DC8#TU&p z>|1yif5TSmcwupDUBQBcwc7Hfotk@0GT+qS2+iRr(7KPSA|^y_Lz5uU5xWY%>2}bHsIjNA|iTVe0e>X09)B-=P;CKg+13_oyDm zeBKjoV2kLO8Zvy2_3EDIW=J{Z$Hws9*Q>ZoM(0G4x4|2O_+kTZ@>MVIZEW@fNf2QY zyn!TQm1*@a=3q%4*T#5(2B8C*MS1Z;i*?@%6LAjRQyJ0UIFLpiuGJYMzowIYF?2aY ziQ>gQj9Y3FdaBSyA6YFv7?Lsu7P(=8#;1L+Be)`;N1_R%DPW`Ub=sq7)zHqyjXorS z+G#XnAJn5i_NJ^xdpG5*o*DEDFXP=GNs`_VMTjyPpURXZ*Y&fNh5Nr=7W(1j3FF2F zPyI*usYg1J=gCH|j-jOC7^t#*!B|m+hFx8W6!X9~0Y*`0Mgh+tVboJ>nqZc917II) z6$w`(fV|9Wbe{rV{sB~rM15O7Y61-B6%mtCeb^}CeF>6%0MWUDOWffH!E9p3b^<+3UmJDEDN^bIe3G_aPJDab+K-g0B6_7EU7RWNq z|3F57!CcX4`OgV`%Kwk4j9v1dYboOYeCo{qMu7pW&Tf#%2Ib*1J+0+57}YR_QQkg} z2g;*MNZkDrS+eb_d%dR8)0~vQ} zdh>4yTHRgxOo=kqB3fe~2A1(H;v%0j%2=3P`jS$ESqi0g*-y_Bej#HFiyx?g@|o1; zzUP&(Q@iy=9A+~gBOMy=eUXTa72@8bFW{K!33lBG?`tAAeZ#AE>vB%*Dgm#;xdA4` z6ELqZTC)bAZ^0?TK}&-?O*qIaXldSdO?bnVkk9&7d#3aPSO{whUDC?!| zY3yVvYfx?mI~mHFRFTC_x^j8XsmWn4Rk@yDSd%BAznk$0<`m_6Zhn;n1-+`mo}=xi z=_KW9Kb;OlU6xI6ZyId6PE^+TV-jGrlE<4Sn!%)qt~WO|nn+~B#^0E*L#95UTz)fh zOw2N8hMyS|W$As&)j#rziEdUzzG<3fwvJ~`k9XHrNF>yCdF?RM26J>A=-}n05;k>} zE+1@~Q?8C7#yT_pNsKjqW-rq+^7Kw(vO%E literal 0 HcmV?d00001 diff --git a/website/backend/resources/post_spoiler.psd b/website/backend/resources/post_spoiler.psd new file mode 100755 index 0000000000000000000000000000000000000000..88adf11dede82b5a915444cf61f13f2176b3ee1e GIT binary patch literal 128434 zcmeFa2Ut@}^Dw@lcPyyb0DFZ5R62wf0@4&vKt+v7fCvOkLKm@uih_!Qg5}ypELT*p z!nI;WMFquzjShlz10nx8ClIjQdwGAq_j~@|_dEo0&hE_Y?Ck99?(E6#9z&}+Zis+H zA1Sy+<6k)>DFU%C7+TpoNnsd-R7VjOAjy#vsG}qhf`5eLCg8XMkDu_PPvqsuO9zLZ zIV*k38OM#sa3*(6$>G5?x<8YrwT$V@Vw;VvyP7*zi^VV->oLoT

bp^kdmYa+q$B z&hGR`f4UK4EZJPaG~75mFgTFOqiKZ)2C%ut;bvoLjG$#qV+dnl;#e)5gy(NI))GT# zc{Mb#!OW(%09WuBAuP)gzMNUte2yrtvI&V?6_{uFqI= z1qhjP7(T|XHnv^aLCkEdACDJoOeBVdh3SON&)=COjb|6Ju)*lg10;xCaFVn2Sn2$1jK%#Ptgb*0Off*P86XVl#rmxKm7tKa=<5 z-@}P%L*p^cK|lK1BwcM?U3ZeMv7RBQNSid%m_+KuL`3-$4=1P#28~Dih06?MJy0~M zHYR*H7?&s|*HRTo=J1oB+Ja8FV5ufEmbS z^SDqSxRKAu%%m9T+ma~OmijX-^>EAjG3k#s0XUl=dI+Za zF18F{Yhz`kPqMYu)wTX-w!M=6!q$bug06@b(2E9>`afa+8y3H0W5?n`LyhRRMp4Vh zZPJao;zww=u?Dbk8x5v$xJ*p*W@Ecm-@6|Wal+EzHE2wuV|Hgw$9e`P!<6^~v`4I; zld*oSw13yGh&cU^aVTkw+>Il@y8@TvljIh z0j&Q66~Lv1{x8)}`8VVKU#j28SWn!s|No=@9;>DM(b&FBhB>j@V7meTFV)z;_j3Q6 z8vAJw{eQg1L@n>fzOQ=}_^Ge_uMO$q5%~ARoEZJ9uO&tqTL%RMah!q}O!FD0#NUB? z4A!&d8MjuhA4!uBA0WbGMNeqw@2{}@r(Y%<@s+0elF>Mled^xeWh}dH2vxltD-M~es%RlOJbUSb%|Bcmq5R|`l2Nu;k-B{rmg%yj3rNPi5o=gt53BjG>=?)-KW}*bJt0Kt9 zjmZlMc3HNZ4oD?52XRmk{P`d)V=ov#|=`vAZNvS{A|z;IY^^ zOTf$H)YeX34x;_dCKw9|ZMgK5cu=nryWo3#xqu&rQCbJ{*xiMILN4QoBd)$&Cvn7@ z!?x~;uz5WZ$7KOrF~-@K7b=cW0t0Ns5zu1~Yb(0HZTEQ}Gu9TOWlGJQmBe#+JzHaPz-TQ_=u2tJ2HxA4L^ z;GY3>PEfE1Y(boZStyI`+pRto4B-kgSq1TUL4g55Y~QXVO5GStgkEsvZn!JU*RMB1 zu^R!hiRrPnB3Ubv0olQw0DoO~yd6!%aVlMz!i~IIw4Yw`D-Kgua_7+4T-XH4WYZ%= zmN*2*Xuwq&xr4RS5F2s9#%T~{LYR&sy7uJH!YK#B70f{>#A66C2qHeC@i@MxAD6QM zk1Jr5*r$t!vY09ymw321u7Rg&#N+b#6)qy)ldv`dEe^{U{}6E6W;|?%h(11m34p7N z2-^iYxF=N%(E|(tEr4q`4OSWg!dhS#1n1cXafG#Sk<>q5B`YxW&rnhPe}ltyfw9Jq zK)_?zQ3&0#i8M^YA(8|_m&P&KF$zp8Qg|9U9MaXJNLT=kDBP`WQS4Wk=v0T^SP1_vQI|j%B|9Vl>xr?#$-vnKYzFtI9)`}-1spCW z)-1)g55S+=4zlWLW0)w&wWkSTaTzY`n`d(82RLG#0O5yxq;VX82fI{()HD zFNhOq8Nl-G9tl-Ng}3VlL&;?sOdnbZ-u26eGC928kk9W1|46R5%opD?*-N!GZK(4h*n_IY2yv0cVFmcs+yz!g#?L9FtmYnLiem zz?z&I2Lyz$Iu`a7g{R{*qOb)PW(2aqz$Gx5gBgJgEPNZn8$&}d!!snDQb_R8X5(d8eJ zz3Bcah}EqK-#8bqGs4hEJ+PjXJ+Pod&= z=~bSgn7#BmNQT9v>tG5MfBuy~O!@bLUis-@`HA-#YuSjJuoh<3blAom!qEc1g3i>^ z{@IBC#|e9h)k}`)u1p^$=D#qt=7W`i`vuj_X0Y%xaaimhc0>2#@IOq}ivt!E*);?P zAL&GDOLWkHCu&Ht_7RdAtcWBR?FS4&TyKu@^I`sgkcVcu$nGHwc>MaqMIsTR5?mHO zEFf!FcP)AdCsafth25mV_fbV^XgJb9xIf@UKZ@Y}r5V(`@bP#}11 zVQ3{)e_B;NK#R9sN@()ZAn8(Ye^T$1(H6J9LZIZnUMFMn-0+%p@5D89Ny-ndLH3GFxQ!$()t>OXiu( zXPIVMS=r&T(`3zLU1VvpJlR;;J+h}{b7c!;OJ&>S6y-GK^yF;i7Ra&X*2*Qzos`Rw zE08OfYnLA&KSADD-bJ1vA0eM8e?Gf6x21o zMny%%v5H2Da}`;Ns}*-E{;7CR@uT8*r9nzlm8_K(DTOE{C>>Y&OX;;zqq4H{L}d%* z1am zgCz%#8*Dw8K6u^WBZKb@E>)9Jo1|v1wp=YiOC{iQm8xYlsWaR1@k zhi4CeH$rN})Dd$>gpJrg;?9Vgk!mANM$$)a9+^3^c$CDbsiRy+MU6T<>d~m?(Hf(v zqk~898GUPXjmA(73ytL(J2i4NDm4ddnrpH&cWUNpR%s2lD<-K=vY!+=DPz*x$;y+-lUGbWH2K98nJI=-{HN@j@^q@i)LB#g zrlw7OGEHKd{xsIKebb&zm!57kojv`~^dfB~ZAwG1SCoUjv zC*~15NwY`+q$8v^x`T9Q>#o(kqT8T1RgbQhruSlo@(kM!Nu|8E9c$}rn_*i|okfkJ{$(d^ z=U}(puE>70J>5RtzJ9jB?C9CI9h4m098w)VIZk!tI9_p*aGLFuuS*ML7tCL9Y(euv%EDa>OBc;r6uYRn%%-wx6U z3J-b~JS{jj_`?e06}wi{a~wFwxCE{@_YzMHoT*14lS4Lyd<-=YO$%)cn;&*Qd|)^` z{87Zzh)ogYk+zXXqoktfQ8!nPS-EEA$7svwgR2OumaV!OGcIO*OzCRs>Jw`e*7&b^ zyq2_f$J(ZK9_y~GAH9C{`p+Av8!}>5VmYxzaVBvGHcD+|ZOq>^W7D2Zotqh(AHC;s=IafR$+oqLVlut;{I(i+t}L*x0AP@*fDTN?_!BvH#40(FfuW z@c&@_@#-M;VAi3DhmsG=91cBPc4YpMyrYIk(~pff7Jp1|oPGSm3D*<1(+$$oPmVg7 zm?4o7l2LhT(W$4WZBAc0qkZP!*`a6S|3rU={8@Ew>AAv8hs>Mj4bPvsFyX@9i-Rw2 z&LU(*WYuT;Wq-Kjb?MpV*_Ur#F}ZT_s?OEp*T!5+yFTQ4Vva)2`W#_yWN!0c!GG1< z@V)W*=8~IlZh78%aeMCV{5uYJ?%lP$dn?Z}FZZ6=y=(W4?_Yjk_#peC{==+CvmRZ1 zJnQkreEs~aCk9V0JvDlI<(cWToC0#ejpsJc@4m2q@$jYd%V&l23SSj1D*Et>@v7o= z!0T_tp~dZQR=<^gyZPOKcggQZy+2ekrR3}f{SVha+I)QU$?emd&-BmLU$|d7O4pYu zmnD~LmZw+fRa~pIt$bRwu&T5=xVpV2wsv6c-mjCtUZ}IEdsOdPU;2&nP1vxdad_kL zrkPDQo98sYZwYK^Z{6HByzRty!|!=~4}N)jSch!KZow46l}?Awx57Z75T9$P;obn| z3=GEOmc!}SHfDPgdgO$#M%ksUy%wPLyXl}I^i3% z9x2Jm$;rzp$;&GZR8&wLI7C%RNp*<&;K4%%4^|(jgnznjqR7uzg0iBbvWoHm6_o*M zDk>^!*hNK6lw@FE7zqDD1LdFq(UOF*NMayCav(wY4(9)I!rKHgNGT1D34B0c>?4T? z5>nDKvU2hYitzC3M|rpBRvZjVT|q|DUe{r*um0zYqqOl65McoRzG9>vX7j#JFMM?(77|eO_0F^ zkexW|W&sFsco>5+PoKy9Bo^WCC79GBg0L7VN#c19L}XMxq;ua>ezu$czIB`1cR!c@ zBPDPvW0ukG;-IIXBTsuiY#C9 za|_RAjJ6p0wLQk=!ei%irw5r7mKPRY$w(8Tz{7%-H!D4Ok^J+obQ^1$1f+4?R?F!5 zwJo^?XWw17ua*1eFnlL7(aSdG&LN}MKI(rqhJW+9k%Yz`dBHz6GOK)9 zUURBH`E^I$gcT`YAJ0o3QLXv?%rVWPH1%&DbMLNu*L-hW=SAO6C*CD92iX%IRm&E= zL42nXhB01oZ(rU$Z*nXIO}&H~Z)p#RIk#N;`lM-to+Uk>9vn4GX?J+%VK4-K!d#b> zHE*8Alx5yZ-Bz&pU|UY?jxyt6g=LFBZhG-4qulR`{)GeYF6MqX09xf$o%%R=#5T6o zirtrXjo)FPaX|Pqx0A>}rW~`pa(J5Oj8*I2E;ZaFWTIQ;o_$2!wumjS>4xOgvd+L=R~vVi9)8|=xw%8(j`lT%`%I;4 zk=q5=wT0-$&?TcBZB19-zS+hw4xdeos9`2Va98d_M_9LBjC#uy;s4rM^r&rCw9QmVOhJoV{{ov{}^>595y&*Dbu6+BtDif%MfnlC728 zoDEyS^3L#t=xlV%urXOn!ip;oM9-JmG`huXtN-rOi_O(XKa}zwnE(0lf^K1e{=j1` zo@Cor)uvM|6K%C5la!83P_|3%&_4A#bNH>DZLy;`d5{(I*M#a(_4}Bl5=-9fL61QXW#hhcG?af=VCYmn1 z9IF0q+EGTqoAi+6`QOIe5Tcp|EBBuBO~@Rl^&PA!WZ3q$qHza4zFxK1e6eq{S4^{p z%e(tRy(XXIVxo%5D@`*Fxm1{UbA(c9B$sb_;g~`^O|$13+>Iz4zxFp z{+>9WOFDP{+`GgbH<;|22-()um|6G#d~B#Oyh=7^H*volW7M33(OKcAu1#kjx4V}Y zJal_)_W8pdqi=Zg&o^%_4jer#eURLko!8%9sGoBFJGo;2`5D2Poo9USW_|L$QB#)4 zPph`63J)o#XHV07M_+jP$f_tZmtQk~f_nbUkWJs2uQP@_sC8tkuH12hf4uO?+*|Et zU-zC0CQM2>ANyJm^ys`0%~<$g`(2whl~$*3+tgQVJy{}rR^6FH|KMFfxVO~EuEn^yTXrXXrWv%hTho3G5>`(j34eib&z z`vNLk8y2X{C;F3b2o#5RZs$e_OlBNsj0$-0c>d~9gbx=4!)Bg*IGK50A>pmQp?~eC zDEXD-q%x)R+rN%_tCui$+#MCwveLCNUxetd$QGVzdPH*kH~y2?-<}j4dgY(ju%JCF zru^!>l2cxfs&)^FUc%3IXq<9*X;$%#>Hhu$=Pl02ZT$4*il#%@{HjZBC^@TW)%MTl zM;N@SYg}0ComWUcH8a{HFXM32^4$+_84R-7TpZ5LEqLeK@Z!=v!}75*hMJCBR%kamu~pqkO)nPhT-(s0386!;3DF+6 z{mm}J3}#5=xoa}5V(b?sZ;Q!jf6yGWC?~Th_-gIhh&|JTS+s_xmRcd2#k<^g>A=Fy zvnCnO<{Uq{EKX4}#_&&ew6mZkCpVgWDR%0h&o`z}$D3I=H0}AGUfOb3bJMarNy+)P zQy91MJdYET5{$@Ar_60PL|k_pnwoh^;Hy%1kT&KoA)1ht{m%P9edy^(V#dVnfvc)& zqz1%P>eem_qErV}b{^BNHQ?3k)0_NN)xYw<;pmvA!t$5Z#pUnZM(1RsmVvj|oy+TZ zdF}ysMVVHB+?T}11BGZdamcsawO`soEhtZ5gk0<*peFHaZq-(pxt_YlYSLJ>zM-W4 zf=TDeixq0#iz(A{4qv&tK;zaYxowx;A5G9Bon}O(n%S;gcvdj`1wSdRx+$A}>EXEp zlKjNh?9P-EN6N;n7+f{e+)2l6%z4Qo-zDiyRmRC9zp+0SpA!^+Y+C&CR#V=)b!kBk z_ggL*)-;5FG5D^Z{r*mce*(X5?ebjB^a6O6kT|aOOrSQ%K7oAv75)sB}pn@Ml; zll9bR`3*B@@OWX7(PbeHL!&&EO?PF zM9cYkyS9ZWhiu4=Il4rk_Vt8MG~@2w&{O^yDjL>JS?hIT!C#t^BRiEVz?*AJ}+Rst*Tvl@pgXo=qsU-+JY}RnY{e1&2dM(i;U@S zTDF85=UqR{;(8i*dDoEY+~_gX%cH_e8T{t7c3DJ61h?$Vjn>z7|KCC@f<;a|`>)=F!3T-4H1T({4+LWo%L zHvGeaw#)0Dr7U?yPO4x{E^P6<`|8%^M8acpttz!-?y(Qif&2^k*&8+INb0YSp0wgp zSbARP!HC7qv)Z!F<6rq-j2%@Zr*)R=DTt|h;!@2U=uh#oSQS6v@LScTLe#Vix>3tV zONWnJ>V7w`H8*3@5@8-XxJH zk5(@#tWEyP{k*L5N?2R&y^NDy?Q?5K8>mGlTu_IB0({!QH~g69a~y%gv9hMUMaxQO z7KQjM_;^DzjmZ|geBKmuc5A3^r4V^eHS(+*sr;8i{7N0)Hkaz`_vy>j*L=J;)uQ=( z%vFQKZR;<;wqSfYHfeKkx~;^d$+8`}L~_9a*^0|Tv>@laM*7klH#8C*@>*Ulw0$bL zqMv8bN`5wfL$k->5a*2TOv9|#TVp?-*Du^L!ZYRu_1VQb!EMJ?>*5Oe)#v!bPQ2gy zDkR&JoF-c-^R?Bab$LnM0^W4s?ee<0c z#T6*4Xg=>eMLv1gXH25zxcgR_`D3}Q)b`{-FUrcO--YPiy7=Y;gAdWl1!+e-+ZTGq zKitT*zOnUU&d96Zmut@Ff4)L=^_lCN;e7hyx_xI#A9XIA?>c+TwoTt|J&ie8q2W5X zG>^D<@3)gXGgmf}a|LzpqVrT|j5HZ>?bJf)3&j)8Xm0DQPK|pRFogH$s>+%RuZ76f zBU#|Zf1LC*GwRI-mcUGtuYVRMHEmkG%^D4Z~HVlLX%T(+Uqv z+v%e_a(3PT!P$Y|Vq(91QA#*}Yg$V~OqG!kdEew;d3C6$C1iE<4W{g1|FbnjR{Pu5 zn3DN-hgIzx`7$()UiGF( zt|X8@#R*?j=3M)D{p{VUSzDc%Y)CrZ1vEyvV*Rl zMmjV`lGESKy&P@0>{@5q&=ICizTdNBe7|3ON;KuxRYW`OX}i^=KX`iWnXIRE{3}Hf z8`2W@ZFzJmjD6`=Zd{q!@%sD9YjY1@i6Wb5!-vVMjWyEaZk9s(q|VPi$DU zpf;g{UzAe*p=8a@j)g5Le+nj!ejHrWVj4a1a;R#BdR~P$edR`#&M%6cr|9pzRIa!G z)iU}JVczAlakeY;@`!~haZMeg6W0pST_aAC`uX_MBO#}ms}DS1EqGG0j(8%*v!0Z3 z{;1ELv8^dZ+nX$P2>bV)?RavSZ!#d~oWT0wZEhxiPU!q8DI;RDNL(R;IZlk-j) z>d%G@%l3bBqlQ{%@O_%cAwf`e^n}xgQnqZCl6e-aBl+OC#GB!QC4Xezz7yqjh$^$z zx-9NlRr!O`1h@PS@_1(5n(4cb1<8`gw!!!LgE)WIbIx=2#Kn7^AA8f<|LLFwiu;d0!*5~jh7Rm`pxX7oHL(FniEgD=Y?!( z9P;$OGJP+3Pe4lN*N%Adqj~(WB9){fmLRQp^X6CO6>lz`+W)??B+<8_b&Bpy_534I z$G?2m*qyL}_b!Xz@xcH`^1l^Va4iF9dY+bImi3hAf)# zV%1rrwKo;hyxB4t<0^G?D*}Y*k4uqy+IO>STE7m_{jzXw=`riT;^$2PEjk$|XdwsE zBV-MZdQ~?)d}NfR{8&mL^S&@yhzjeLebH+Oc=+Jz)W4)tUR5t1`N!La#kINjTPJ?G zw<3SWYQxOi)k}!GI~(cSZ?x`TGI7*vS)=<5&69hD=s;rKvWM;G^Z0s^of!uxuiT3^ ze<)j`GVt}5XQze83}(^iR67a<`?8xl52TM4cr|B18vcZ`=wT;3*1VW}?|N)a`H*WgE>45iD921uI!tap7bASxJnvC-P=)jL0~fv#nyVxW{Uf*w zn*xIxfa#N%7v9G9Pxq`}zxMTC&J!nmsL!md3eV5Ghl*t@7vB9?T=n$N^W2;>%zFa; z3CHK&T1;;8u3fOiGw0MBr?_<99}6r?nA1y-U*DPdPQc%#WPUs`^;TkXVOE~sg1SG1 zsH*ex%ZlCk`m;8DKm7X1PT$SmSBD+GpK&~zRaw&%G=0|9(A8}J!)$mqs8Vv$j5{D@ z>HFy#$}4C>r1#{R5Ivu#`FsZHOuLK2#T$=1?#Cs5xmaQgUe$z$RN3n5%>!NB@+RD} zYEeGh`O>9Dh*A%KxKH3`PSAWe{qV%;2l7>huc~JmC!XF}Q4^M9!UwKfS?g9^^g6s^ zaS53kBv?@OE$2MXakc)Jtjrkw*Nm0#YG5LIM&;n@kJ|T-Bu*PCL_3^}8kE1SXggI>sQsIs;~ze&WlV^jJUczEJD&&qAE|DxKHU4CdE8S$^~}r?E_R6wPh) z2KktXk~ZSqarOLL$v%rNB)Z+1u17vjXg`oElWWC){X8;99sCZ@)W`XR)0g?W-4MtY^BtoD5(Sg$ys7SCFP!57!-uO| zRntq(9m*qV=3j0xJM*^gXnVD9Rk_uTirrsto&Na!34KY@Qoc{vtTbxbi=tVbsUJK4 zjCQWNRFq2`9OHWDQcC=uy7O;iZeHA94n1YzQPO_XrA3#uFE+QY&^%OrYfouJKF9N} z^@F9G^b)gNbRr(TCO^oJAy%w9+f-H=qIxoVWCpLinV2!uVUNkVBirm+jjv~|O3ujT zHVzx1mnk#lRx1sFjC_z#pAL~nYpQcVB&m6BB9=o^x zR+Ap}-u)Ai22PjQNAH~P-1_lW$Mefd0`IdITU6t%9$(xygZIgMo6DMXXukXAeTCuD zQ6^y3mM=p)G{~EGd%Zev;@G%B#)(<)Yz5v&mbV84H=rqk_`}PhPQM-}nAWP$RQBpX$pT_>x^|bY@YZ^Thc>4qe+^;MTw5DVj{b@|0bMrkk^b^sT&ZQ`Bn-w1JI&UrTE ztr~aJS5R?1sAExCME-)YvKr3xqEVePxt^KEK?}|rOf7on zy{_Os_kPs%8QOvQnm+;W4?<)yA( zpS%piEs|S@D_#{$I{Gj!sWsn=aU0zolcpv(pAg;|Fk>gPA-Hq>zAZv@dSA?mIV&Uu#5 z{N(osp+~kqB*grq|B&nnLtfZNh5$>6;7-8SH20K)pwUwf!o=R?kRWvPAm~W+@Wz?J z1=et48tGnRuk`_;wkDwfAl)C)FRxPA`gY}u7?AW0OF=TgbV53Y)h8R?;|0DH;0WWp zl1&z^pT|=D^H<6-$TtWQVSEBY*szWgUvDaf9Kis5Q4kJ~U@-uW{WgrR0;dNo3&6sd z;2568_JQApaXcRPXZvF;F?tbSZm`b}ae4p;;+Vkr2jju@3&irk^?}9dAzXYdCctLG zcL02`>>w5P3OxpF$moK|2VgH4ac2cGvArUeFOL+G*7>&J`TB%&FyqD2{_ug-^cCy@(x;!Hy%XAC{K)d6NwA6@R)n$#es-yh8)I{t*8QzL-seb>r9HUBq?u z8>AFytV`}6u&@R>f4A@nweFX@gz%ZW5t6;>{PP{b@7Ymf2!@r0Ki>gk+`N{6*sp%R z|6;q6$O_+EBC@?6+f~5r>i>4z^_zyv&1LJ&0c~P!2;Ym+ZOeak)kJQ+NY_}I0wO}O z_Jq-{g)df%Sbbt`G6LZ}hXTGAgYW>WC%9t$>jNxhPtP&72B@)5pDkFn>m557ANL41 zKZM2{K?HKyANze`OdfUJALfeUPrBkyaI7_w&XUTTMXyox-32X*4yw~0h1qvJ9!H2z5lE!0T9g$|S zf`X~3f;_Pc!J_jN`LXviwI!iXe|;>0I6Uj8SH>yF}WguNW3l? z#5bZPV>xuh_$wrTHEny}MX@dI16hVqXyuFPW%(@B z3Y+mO&<$IOZpDN*Xfl6bfq^Z3cuY=js3q*&v5N?X{d}->M~lY^VRmOJhB7>YI1D#d zBvZ?<8#jl;6rr_dh(SSI-p?>tn$Bg?LwKyvE^@AxUN`*$*rTE~L%*kpcrG9#wx)fd zmX0A_Z@M}&W^`AcJp8P~1M#u)iHN!@vZpe^MyUZ%oE{pi_Pf7bRv=HrO6|y`VT}`z zVwt)?*#4THTCs!I>0xv}v;eMHIaVPY8kgov64>4L z3kkOhf=#!*Dnn!%4onUktMcB>p&Qo2lNAtv@4D*Bq`TN)g17-;y2L-k`Luu#W_OR& zZHv8nrEXOv-_gcm5TU4vqh-A<92?BsfTp|wPy0CjP zBf9wog<;@cAS{aOPq#~dc)IQuq^BoRDh?E@q`UL%rHmd;^lrIgO^C|Xo3(9k9siFR z;ydiH=4Z#mcC)z$Va2fxp!xQwSfm~pY*|1|6LnL>d0eI~NAwa{S^(ayc(52ezn)a^ z0$=ElWUF1ABGYzBVil?3Bg`MfOCr_G1MwU}KMH z;OQQDpyS6l^zM+b(GfOw;+b^WA`vUwPh+J#@o#bhBkRelyK5C`9zz8M`Lnuuxh|0b zI;Z=6%Uvcd#{5P;j2yZXo)wMD{AH|#_CUlCU3|pF#NfY_6a(Trwy}3X<2*^dM)mH# z{1+nKu3W`tC2E8}a?<;qXx&;6x5q9(*A34R4`UbXD}I5qO)syiYY2do6!5wf>57OQ zx`6vuzdpinKrDRMc@Xl4^A+sy`2hp-x#Wq?LHNLrB_t6@5<(@R3aMQWDI}0O zU<1f-tPrQY0ypR7nF#0#YYwA89{Hnq*0`CQ(RKl0At|T1VPIiX$bE z4w4R&j*^a((n+^SpGl>pa?)2)GpU7?M%oK&WyhdJQ;;_-N}i5%kTEhDBRNJM9TG}w zk-B99uLF}vOI>S}P>!S~G!ZT&$&nODN+cE10JLZ_T8N^Ar9vrmSg3`LlLiSTMJXMa z%wRm>5UmL!NcY&kAYQkS|Dd>kQ(8BRe=Mhn=sznGWNA= zao>sm;Jk|m19{vidu;JPHm#nd-L7)C8R3p>m-@wStZtXb1%B?YPAm?y=h5JZh8`F0 z$2pJKvF+BDcuGiwqY7A&;4VYcqSLS`L6^f!#118ZDeKQ*7+2~3n6upF$Y4{eZeO`4 zn{G%K5#|E~fYajU$MWf!=i*MgH7%Ii%?m4UH)%I)9_DdjX?vH=jmKg6GkJb+ZiTO3 zZ-gVum&Oa>fcM=ai%k!(w?TA?)Z>|TDczkL0z)@AtfE$e*A(`c06XOYd-%{br6oGT zJTZjk+dXrHvD=Enr1|%j<_C9H?EL&NUFdOEBFxhz>NaAB#Z~`O?t+vS@V7d!|FbBvSfM?GJArf8%ypf{Vq1PMrzA7SZ9V z2fhXg9>#Wc?QXz1xNE|Qn_znlaMr9BAghEbSWDCOxyW1c6 zJ%7CXec|r}4@)_~l?K0si4Nt`hJlF*KZ1n7AO(jNi3X_uFc(p$6#~!NxOnmL8J^B8 zL+qIj(X9s#`W78901sojaLaz1q6_*fJq*+V(6B`r7<4($HbHb~S!|vwlM4qZz?`Q0 zxn#cw-S0uefbzRZ0dBJW9`yfYmeB7(cMagbc|6|lK|_1%eh}X66ZU)1{T?*@=F;y$ zi=W;9y*u9TLI2+O?0O8|??L|`@Sx4j@ue-;5+czO7pX8BzNknVTO5Fe@wsZ(>VX~r zVC67u_bGu`82^b1TciZY?vE;lm;;l8VC<)gP<#zjHxBMF@ zz%0y94b9xBX!n=pO81fqjTumxqu^xD0dVG-3aoeQ`BQ-KC^Q<*K^qF^A`e4D;7l|% zGy#o;KMlZOe`TL0r&1d zbvQ`{%V{XwH6i^ZGzI573Ubka_(XtAg;hBtyK=?gJ%3n=agYL&cL>fy9l}@%l#pUq zxs;)d%J8QG zIXBsRasettk`m?#_dW?FU+lJ5Dnzf4gxl^9Z-tUI2bU_nh7;Z-rfn=K6iU>j7{5X9 z5W(cwQz7A!(>qjx2-HiDgoOL^O3+6{u(@$hNGS05h(062@bue4!d30h=nEnQHQo>s z)=QM4GDOh3og*Z?wl71lGB4zc5N#_*m588n;gXQ>WPBy6LWEf*pM*-SpI_wVT*y}ulNwgwqRpSNW7eAE>(S~XSOPgZLg_3#UR?;=F zYEhqkwG|e{AFV-f((#T;p~QXmxG!)dCH*q&UOQKd;3VTC)li~s|3OX>)M})~&V5Rh#1_Xx@Ga;1n6{(IfV{hJf{LjpDCl723rA(Fp z*&+e8wGlNTG__7h=$a2y&(MLPFl;MmU7V z|5BAu;=I2qtP>q@0iYe@nt=jctB1W;fFHW?Ynf1@HcA?V+zGIc&2Ty&;y`F$BO)ws z0F8d|0$c<@4z!?FI7kyh@lA+8y8d2BIHm#xVF85Nis7Dp!*PiAMo9QP15iHSgoJvD zHq?fYFW?S<^lM+iPHaGhKzw%_`i=;yf(9YV0@QZco?!u~?SRt$4k&aEFfRbJ`vvUk z0L)p4zy6N+unrVxMnXZD&xM2rRghJp0&q+C814y#+yJNZ4D=7FrbE1*kJ=G3hEOW- z`}|ahVgTb0ae6z3x!EX09{{r}Ux-El<_g3s+7T=R)dm_p;5HIA%3K4?0EjQ>z%b{4 z<}{>9sTRt;Kg?b@cRn-rezg#Cfnoygbq9jQyQ6@9Fr*&@Hj-!Ah6rP}f(;acQp)Z$vzA2Il!eDNQQMHSe)tr zp-7;Z449K(ik@)S0Ls|_55_6>0Fx$+Nif)uKoYDt8;m9hxV&tFa|&>patI}ZEuIF< zJ}`eBxIX}jphlsD8mu5j$O!n&0*Ye5=!0p#g!{+_L>S!JB$R-2aDgNqm=r**C^Um{ zltY_~2O2-%aso&$;3Q9gWCd`U0hnB%mAi!S%JXTBEu>Hsl;>CcziO$3IGt_Q0RCA{kR?UE34Cs4+c#7`8 z^*KN-LE?h|bpfOGgZm|bzr|VP0m~Y|MnUOk!CtRmz@7vwbjNVb0c8m*tPRki zfIbL_T2u$u%YY_=wcoY^YuDHU-D^1D(*OePm~N5U<9L#ddVXp z8Uvb_#YE491_qmHU?dt`kPmy*fLtE{`_OE-MpnQvV5gtJ21fud1i(5p9j;kr2(5w+ z2WyfWAU=nngjHfyDI!cbQ2>nq;P(JNiGXgvO&112o2+u^K?PtM6v$Z~$O+%%2+(yw zWBr6Abhnk&2qpibEE7Qg+A0r5V+S^U8EkqHP~?0-(lVxjnQuFqg);9>(qPlYBjA(+ z*??6MHUZgaxOWbGiZ#xR1*i=+bn-oH#%TodAJ?HRNJoNPhsz0>oIz zXcn~24iyZ8S?KMDaPl}{9zzo7TO@MvmQ8_%JLxeZ3=sgz7YZQP3bHstbWFFq#PH7Ypn&rUyy;_A*ANP4{4`@S@Y8FXfpsB0_hJ`@sF2F_sY~)DB zI*CT1Lety(myV|HIeojh1>6VBI{W}zhL08&Fb<4{?#vTR!5EZ6XaZDl2R8U%%pPDU z7_ed?(LOA5DIDblsjxoX6Aa}npj;px-j0oOA0cD_FF2I=xKK74FUy$zvU4E0h-_%s1fdjL>|54R&>P)&t#+_O+fcs>|VYyen& z$A<3>5Q>DmXAyX7l7N~AfQxO|s9y#l7BF52Bl`yERqVhh1)wfK8H3&VgRBdSg@n7t zfO7xoNE1C|ZGOS2+| zCX8%_@JU5JlAOssQBxt5Y(B~W|dHqe`K!IXCz^j^tDPz zcnS)DJ}w=K`sJY4@G2y!>Y7wsAtXGIt;L+(jb%b<{-d1?ivdkYTEW13Q(k=;979o! zImz?*pM@%o#rH0s{A2H)v;!xyZx_{mf?lKw^QI@g0B^dQT#5PEyRX2gs8)fw-g8U9 z^=|MjgX5YBvfKU=64FPMVy?RJLvYn!yL`r6c%S$8gc9E(B|f1KaF{#e%_FF&P_+-3 z*T4AwQ=!D`6^ie%34!C84zRQ93*MmDNJ1kxt5_&;GfMk4HkHt#XMBaJL|azyzrhQab*4!QdQmqozA4>tN>xw^sMQn-tx#d4zK{FmZXG)U(R2W~IH z;jU;t@@`x`eSP$7E8E$|PP6q~EbTpJQ>?5_EvVGlW;UkA4zq12Hgjz#w)RvT2RmC^ zDwS_5q)xG=Iv86~?Wnfa_Er>Iik+>Ml{v)hW?9%cI9QoE+t@feQ*CYS?UZ09vU>8r zW#9@K06B%yX^SW=wv%jt7Zqb}`R~lN{)xGR4TWOgXe*#J*=ka4AcudEbkyH4Gqo`> zaDdvV2a~HE?;&&u%Pl(^Ru_aEpaZ-H3wxp!3YdA~YtPIU4c*WXM?exgMvlO*o zm|Iy`gIX1-)<&kz%*ZV}k`gyn#3^n{0G6x#T+IzFsRG+URAbBEant;nn>A)wwp2?) zORtdlUE4SD<7792c-wa*B?&+iQrowI)ciR4P1|-QgnLsAEg*r^HpXw?xg#+yCNhNW%jl#lF?<4ek*hW&>`2z`ZA$llV=o@a|yij|?A&)Qwv;$l1Fls6{q+8!I`PxEr3SdmTn z#>yt$405%i@h}ks+>wOA}`yRoDeIB zQ{B3ATTI}>*<>(o3u|aY-)*N-Ftax{Hn;O!zA|A~LR@TITO9Hc+S5*0X

l?n0jLtY_Y0t72y8xMEupR=zFCQ53@tTWKhD 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

$b-W}j+$(*~rzYgDP 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 literal 0 HcmV?d00001 diff --git a/website/backend/sass.sh b/website/backend/sass.sh new file mode 100755 index 0000000..cf18c07 --- /dev/null +++ b/website/backend/sass.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +sass.bat app/src/sass:app/static/css -s compressed --watch diff --git a/website/backend/test.sh b/website/backend/test.sh new file mode 100755 index 0000000..03102b1 --- /dev/null +++ b/website/backend/test.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker-compose --env-file ./.env.test up --abort-on-container-exit diff --git a/website/backend/ts.sh b/website/backend/ts.sh new file mode 100755 index 0000000..b2175b4 --- /dev/null +++ b/website/backend/ts.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +tsc diff --git a/website/backend/tsconfig.json b/website/backend/tsconfig.json new file mode 100755 index 0000000..555521f --- /dev/null +++ b/website/backend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "outDir": "app/static/js", + "alwaysStrict": true, + "noEmitOnError": true, + "pretty": true, + "removeComments": true, + "sourceMap": true, + "watch": true + } +}