Browse Source

Release 1

projects
Domeniko Gentner 11 months ago
commit
9ac6e9930e
  1. 8
      .gitignore
  2. 22
      LICENSE.md
  3. 20
      Pipfile
  4. 251
      Pipfile.lock
  5. 130
      README.md
  6. 0
      db/keep
  7. 35
      docs/apache-config.md
  8. 1
      docs/flow-chart.drawio
  9. BIN
      docs/flow-chart.png
  10. 84
      js/labertasche.js
  11. 44
      labertasche.yaml
  12. 26
      labertasche/__init__.py
  13. 12
      labertasche/blueprints/__init__.py
  14. 189
      labertasche/blueprints/bp_comments.py
  15. 201
      labertasche/blueprints/bp_dashboard.py
  16. 46
      labertasche/blueprints/bp_login.py
  17. 12
      labertasche/database/__init__.py
  18. 190
      labertasche/helper/__init__.py
  19. 100
      labertasche/mail/__init__.py
  20. 11
      labertasche/models/__init__.py
  21. 34
      labertasche/models/t_comments.py
  22. 23
      labertasche/models/t_emails.py
  23. 21
      labertasche/models/t_location.py
  24. 30
      labertasche/settings/__init__.py
  25. 9
      mail_credentials.json
  26. 75
      server.py
  27. 16
      server.wsgi
  28. 1
      static/css/Chart.min.css
  29. 10168
      static/css/labertasche.css
  30. BIN
      static/css/open-sans-v18-latin-700.woff2
  31. BIN
      static/css/open-sans-v18-latin-700italic.woff2
  32. BIN
      static/css/open-sans-v18-latin-italic.woff2
  33. BIN
      static/css/open-sans-v18-latin-regular.woff2
  34. 5
      static/css/sass/bulma/base/_all.sass
  35. 143
      static/css/sass/bulma/base/generic.sass
  36. 1
      static/css/sass/bulma/base/helpers.sass
  37. 79
      static/css/sass/bulma/base/minireset.sass
  38. 15
      static/css/sass/bulma/components/_all.sass
  39. 75
      static/css/sass/bulma/components/breadcrumb.sass
  40. 83
      static/css/sass/bulma/components/card.sass
  41. 81
      static/css/sass/bulma/components/dropdown.sass
  42. 77
      static/css/sass/bulma/components/level.sass
  43. 52
      static/css/sass/bulma/components/media.sass
  44. 57
      static/css/sass/bulma/components/menu.sass
  45. 99
      static/css/sass/bulma/components/message.sass
  46. 115
      static/css/sass/bulma/components/modal.sass
  47. 443
      static/css/sass/bulma/components/navbar.sass
  48. 150
      static/css/sass/bulma/components/pagination.sass
  49. 119
      static/css/sass/bulma/components/panel.sass
  50. 174
      static/css/sass/bulma/components/tabs.sass
  51. 16
      static/css/sass/bulma/elements/_all.sass
  52. 24
      static/css/sass/bulma/elements/box.sass
  53. 325
      static/css/sass/bulma/elements/button.sass
  54. 27
      static/css/sass/bulma/elements/container.sass
  55. 155
      static/css/sass/bulma/elements/content.sass
  56. 1
      static/css/sass/bulma/elements/form.sass
  57. 21
      static/css/sass/bulma/elements/icon.sass
  58. 71
      static/css/sass/bulma/elements/image.sass
  59. 50
      static/css/sass/bulma/elements/notification.sass
  60. 39
      static/css/sass/bulma/elements/other.sass
  61. 71
      static/css/sass/bulma/elements/progress.sass
  62. 131
      static/css/sass/bulma/elements/table.sass
  63. 138
      static/css/sass/bulma/elements/tag.sass
  64. 70
      static/css/sass/bulma/elements/title.sass
  65. 9
      static/css/sass/bulma/form/_all.sass
  66. 22
      static/css/sass/bulma/form/checkbox-radio.sass
  67. 182
      static/css/sass/bulma/form/file.sass
  68. 66
      static/css/sass/bulma/form/input-textarea.sass
  69. 87
      static/css/sass/bulma/form/select.sass
  70. 57
      static/css/sass/bulma/form/shared.sass
  71. 215
      static/css/sass/bulma/form/tools.sass
  72. 5
      static/css/sass/bulma/grid/_all.sass
  73. 504
      static/css/sass/bulma/grid/columns.sass
  74. 34
      static/css/sass/bulma/grid/tiles.sass
  75. 12
      static/css/sass/bulma/helpers/_all.sass
  76. 37
      static/css/sass/bulma/helpers/color.sass
  77. 35
      static/css/sass/bulma/helpers/flexbox.sass
  78. 8
      static/css/sass/bulma/helpers/float.sass
  79. 11
      static/css/sass/bulma/helpers/other.sass
  80. 2
      static/css/sass/bulma/helpers/overflow.sass
  81. 5
      static/css/sass/bulma/helpers/position.sass
  82. 31
      static/css/sass/bulma/helpers/spacing.sass
  83. 98
      static/css/sass/bulma/helpers/typography.sass
  84. 122
      static/css/sass/bulma/helpers/visibility.sass
  85. 6
      static/css/sass/bulma/layout/_all.sass
  86. 9
      static/css/sass/bulma/layout/footer.sass
  87. 147
      static/css/sass/bulma/layout/hero.sass
  88. 13
      static/css/sass/bulma/layout/section.sass
  89. 9
      static/css/sass/bulma/utilities/_all.sass
  90. 5
      static/css/sass/bulma/utilities/animations.sass
  91. 50
      static/css/sass/bulma/utilities/controls.sass
  92. 107
      static/css/sass/bulma/utilities/derived-variables.sass
  93. 115
      static/css/sass/bulma/utilities/functions.sass
  94. 78
      static/css/sass/bulma/utilities/initial-variables.sass
  95. 285
      static/css/sass/bulma/utilities/mixins.sass
  96. 87
      static/css/sass/labertasche.scss
  97. 1
      static/css/sass/sass-watch.cmd
  98. 86
      static/css/sass/triangle.scss
  99. 7
      static/js/Chart.bundle.min.js
  100. 21
      static/js/dashboard.js

8
.gitignore

@ -0,0 +1,8 @@
.idea
__pycache__/
venv
db/labertasche.db-shm
db/labertasche.db-wal
output
/output/
*.sql

22
LICENSE.md

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2020 Domeniko Gentner <code@tuxstash.de>
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.

20
Pipfile

@ -0,0 +1,20 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
flask = "*"
pyyaml = "*"
flask-sqlalchemy = "*"
flask-cors = "*"
antispam = "*"
flask-login = "*"
sqlalchemy = "*"
requests = "*"
py3-validate-email = "*"
[requires]
python_version = "3.8"

251
Pipfile.lock

@ -0,0 +1,251 @@
{
"_meta": {
"hash": {
"sha256": "57134ef6f8a30aa46c1ab6263e62e14edbb27d6df2911fc6b2140dde8c49d27c"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"antispam": {
"hashes": [
"sha256:e188b424ea9b76c408a592a5ff60eb1280f45f26b404db4d5e96123f485de39b"
],
"index": "pypi",
"version": "==0.0.10"
},
"certifi": {
"hashes": [
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
],
"version": "==2020.11.8"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.2"
},
"dnspython": {
"hashes": [
"sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7",
"sha256:40bb3c24b9d4ec12500f0124288a65df232a3aa749bb0c39734b782873a2544d"
],
"version": "==2.0.0"
},
"filelock": {
"hashes": [
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"version": "==3.0.12"
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
],
"index": "pypi",
"version": "==1.1.2"
},
"flask-cors": {
"hashes": [
"sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8",
"sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324"
],
"index": "pypi",
"version": "==3.0.9"
},
"flask-login": {
"hashes": [
"sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
"sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
],
"index": "pypi",
"version": "==0.5.0"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
"sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
],
"index": "pypi",
"version": "==2.4.4"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"version": "==2.11.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"py3-validate-email": {
"hashes": [
"sha256:3bbb264b49c0ae09afdb2738956f00b0e8dd7e079e2d079b2e9b6688de474d28"
],
"index": "pypi",
"version": "==0.2.10"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"index": "pypi",
"version": "==5.3.1"
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
],
"index": "pypi",
"version": "==2.25.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"sqlalchemy": {
"hashes": [
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
],
"index": "pypi",
"version": "==1.3.20"
},
"urllib3": {
"hashes": [
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
],
"version": "==1.26.2"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"version": "==1.0.1"
}
},
"develop": {}
}

130
README.md

@ -0,0 +1,130 @@
# Labertasche
A comment system for Hugo, written in Python (and Javascript).
## Feature Set
* Written in Python, utilizing Flask
* Robust Database handling by utilizing SQLAlchemy, which supports all big database engines
* flask-cors for robust security
* Uses Javascript to send comment via POST to the comment server
* Has callbacks for implementing your own notifications during the posting process.
* No IP being logged
* Email confirmation
* EMail Blocklist
* Only outputs JSON, so templates can be done independently, enhancing customization. Using the comments via a partial
template in Hugo is the recommended way. See below for integration code.
## Requirements
* A public webserver capable of running Apache/NGINX and/or gunicorn. This server does not need to be the same as the
server running the site, but it must have access to your CI/CD chain. Same server is of course easier to implement.
## How does it work?
A picture often says more than a thousand words:
![flow chart image](/docs/flow-chart.png "Flow Chart")
In some words, the user sends the comment from your site to the comment system, the comment system does the validation
and confirmation. Then, a json is put into the data directory from where you can load it via Hugo and generate your
template.
## Setup
Run `ssh://git@git.tuxstash.de:1235/gothseidank/labertasche.git` in the directory where you wish to host the comment
system. For example, `/var/www/html`, I also recommend making use of `/srv/` or `/opt/`. It depends on you.
When everything is downloaded, create the directory `/etc/labertasche`. In this directory, we need 2 files:
* labertasche.yaml - you can find an example in the root directory.
* mail_credentials.json - you can find an example in the root directory.
Copy these files from the root directory of this app to the folder `/etc/labertasche`. Make sure to set ownership for
your user that runs your server later. I always do `chmown user:www-data`, so Apache has only group rights and enable read-only
for the Apache user.
Make sure to read the config and replace the values as needed. The mail configuration should need no explanation,
`labertasche.yaml` has comments. Feel free to bug about more documentation regarding this. Pay special attention to
secrets and passwords.
Now, for the server there are several options. I personally always host flask apps with Apache and mod_wsgi.
The config looks like this:
* [Apache](docs/apache-config.md)
Other options:
* [gunicorn](https://gunicorn.org/https://gunicorn.org/) + Apache/Nginx with Proxy Pass
Once you can see the administrative page, you can start integrating it into Hugo.
## Integrating it into Hugo
### Javascript
In the project folder is a small javascript file. You will need to load this into Hugo, I suggest using Hugo's asset
pipeline to integrate it into your site. One thing is important to know: this script only does the bare bones post request
to your comment backend. Anything else must be done by yourself, but don't worry: The function is making use of a callback,
so you can control what happens during the various stages.
TODO: Example using the javascript properly
### Hugo templates
Remember the `labertasche.yaml` file? It asked you where the data folder of Hugo is. What this program does, is to place
various json files into that folder, in folders that describe your sections. So, for each category/section of your blog
where comments can be placed, one folder will be made. And for each page within that section it generates a json file.
Now create a new partial called "comments.html" (or something else). Within that template the following structure is needed:
```
{{ $location := .Scratch.Get "location" }}
{{ if (fileExists $location ) }}
{{ $dataJ := getJSON $location }}
{{ range $dataJ.comments }}
{{ end }}
{{ end }}
```
This loads the json depending on the rel url and walks the list of comments. You can then use the following variables to
access the per-comment data:
* .content => The body of the message the user has sent
* .email => The mail the person used to send the mail
* .created_on => The date and time the comment was posted
* .comment_id => The comment id, great for making anchors
* .gravatar => The md5 hash of the mail for gravatar, if caching is on, prepend e.g. `/images`, otherwise use the gravatar url to integrate it.
You can style around them as needed. You have free reign.
Of course you will also need a few inputs and a button that submits the data.
Here is a base skeleton to start out:
```
<div>
<input type="text" maxlength=100 placeholder="Enter Email" id="labertasche-mail">
<textarea cols="10" rows="10" id="labertasche-text"></textarea>
<input type="button" onclick="labertasche_post_comment(this, labertasche_callback);">
</div>
```
Please take note of the `id` on each element, these are mandatory, as well as the function call for the `onclick` event.
Again, style as needed and add more Javascript to your gusto.
Inside your template `single.html`, or wherever you want to place comments, you qwill also need this:
```
{{ $file := replaceRE "^(.*)[\\/]$" "data$1.json" .Page.RelPermalink }}
{{ .Scratch.Set "location" $file }}
{{ partial "partials/comments" . }}
```
After that and configuring labertasche correctly, the json files should be placed in your data folder and all you got
to do after that, is to rebuild Hugo and the new comment should appear.

0
db/keep

35
docs/apache-config.md

@ -0,0 +1,35 @@
This is an example server config for Apache Webserver with mod_wsgi.
If you wish to use pipenv, then please take also a look at the
[WSGIPythonHome](https://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIPythonHome.html)
directive.
```
<VirtualHost *:80>
ServerAdmin server@example.com
ServerName comments.example.com
Redirect permanent / https://comments.example.com
</VirtualHost>
<VirtualHost *:443>
ServerAdmin server@example.com
ServerName comments.example.com
WSGIDaemonProcess laberflask user=user group=group threads=2
WSGIScriptAlias / /var/www/html/labertasche/server.wsgi
SSLCertificateFile /etc/letsencrypt/live/comments.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/comments.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
<Directory "/var/www/html/labertasche">
WSGIProcessGroup laberflask
WSGIApplicationGroup %{GLOBAL}
Options -Indexes
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/laberflask.error.log
CustomLog /dev/null common
</VirtualHost>
```

1
docs/flow-chart.drawio

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2020-11-15T15:20:17.771Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.193 Safari/537.36" etag="1QcwDtM_10p-pEkd1fcO" version="13.9.8" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7Vttc9o4EP41fMyNLdnGfIVAm14uyZX00ny6kW2BXYTFWSJAf/1JtmxeRBsnBQSezjCMtJZsa3ef1b7ILdibLj9kaBb/RSNMWsCKli143QLAdgBoyZ8VrQpK21OEcZZEatCaMEy+Y0W0FHWeRJhtDeSUEp7MtokhTVMc8i0ayjK62B42omT7qTM0xhphGCKiU5+SiMcF1QftNf0jTsZx+WTb6xRXpqgcrFbCYhTRxQYJ9luwl1HKi9Z02cNEMq/ky9PN6oncTrwPn/5m/6Ev3T8f7/65Km42eMuUagkZTvm7b02GV3Nr6PUG7FO3C1CPsvbkylFL46uSXzgS7FNdmvGYjmmKSH9N7WZ0nkZY3tUSvfWYW0pngmgL4jfM+UrpAppzKkgxnxJ1FS8T/lVNl+1n2f7DVb3r5cal61XZSXm2+lreQHY2ZsnuelreK+cV65OL2lGHV3ipxjE6z0L8EwYq3eAoG2P+k3GgUhiBNEynWLykmJdhgnjysv1ySKn8uBq3FqtoKMm+QcqeUSlXkn3euPKKlDcE+7wl1/OWsmtSyuolXxCZqycNCGITQRri7AVnmhKsRSylsogTjoczlDNiITaDbXGOEkJ6lNAsnwuxHbm4LeiMZ3SCN650vDZEXiUU8WCOl+8Qi85GdRfgq5WqHclR3cXavNueosUbpr2cdnDGA43xn7FYNsOC+JDR5aopnHfPjPG2rXH2t2F7k2Fzaxo229qvGKexbK4GMHGbkE6ngglMNFO8aAGPiAV0A2HmvLFsPdwPHw8KvAhhfxTuA54X+jgYndDkVTAzBz2jnqPdeofnaLXOynO06zoVNjSJPdu9lBDhfARm1Nm3dT8wYXJ9MzRtwYFuEmM6DebszeZwNBqBcK85jLzAc4/ph0CwbQ4rM7dhDv1TWkMImu6I5L0HnCWCYTg7FeJATcQBzyji9ADgjh4MZ8Lb9yNnH858EEDvmDizzw5nF+PwHx0asO5m5BuFBtSg8YzZ4fYgP8T796DAdx3XOiI23HPDRpnRPn9sXNYe5F3GHuRpQGPidcUs9m/u+cmyi/zLxGUtVk5SJgDSkkWPJH9Umg8OCA0nJGFcB+yW3lwMaHcdR88yHUcDoztaI+Jovy5AjeawgNkQ4T2CPrNSW21B2x2jltjXLHEefyepfBohdCEECqzcrDYjHN91hZw9hYHTukKw6a7Q0bHWqYs1s15PR8NaMyJv6J0bpMymgRsAKVA3fQwco36Knj5uSMTu7Z4bMI4pv+mYMhKxA6cu0IzuXeVrbgAt9w51qP1CrG1wC/Occ4u14eWcgxOdk8OmbqILGj3l5hgV4psSJnuEeK5JlNrCd8w6J3qW8+Zu2P/8KGg3d4/3embzuqupC4vRTDbDFUmEVmTwdasaFPpzG1QEFE7GuVbdz7m4DVZ0ViiQ7eqm2OC5Pbs8xFGd4ddNcWePJa7qf4dH8e8I/RcRC+uGE9BoARDq4UR/OaN5sSGvMXycj2nxYGB9Y1QmyZKR+EupHFLUMFB1fuWgvpG583zuTnjf3lM8PLFvpJ+xHOA8USlNHU6jprB+JwjsGD9J6ZgtDDTBFNYtDDg/UI0TmUK9MKAMHqEoKrC2KvjCE0SaArhdW9cBxhFna4LozhMiBfCEAyb4KpUpFvwex6LVEwMGvZuDysPkRxw78rAt499xOEaTy00ogjt1c17QaBHc0XNeX5j8Mk1+/jtKsinLm/mHHaU5LI6kHBB7oxH2flApbXcC66jnT+zT+X2iu/42Ob+28YU37P8P</diagram></mxfile>

BIN
docs/flow-chart.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

84
js/labertasche.js

@ -0,0 +1,84 @@
//**********************************************************************************
// * _author : Domeniko Gentner
// * _mail : code@tuxstash.de
// * _repo : https://git.tuxstash.de/gothseidank/labertasche
// * _license : This project is under MIT License
// *********************************************************************************/
/*
Callback example.
Possible messages:
post-min-length
post-max-length
post-invalid-json
post-duplicate
post-internal-server-error
post-success
post-before-fetch
function labertasche_callback(state)
{
if (state === "post-before-fetch"){
}
if (state === "post-min-length"){
}
if (state === "post-success"){
}
if (state === "post-fetch-exception" || state === "post-internal-server-error"){
}
if (state === "post-invalid-email"){
}
}
*/
function labertasche_post_comment(btn, callback)
{
let remote = document.getElementById('labertasche-comment-section').dataset.remote;
let comment = document.getElementById('labertasche-text').value;
let mail = document.getElementById('labertasche-mail').value;
if (mail.length <= 0 || comment.length < 40){
callback('post-min-length');
if(btn) {
btn.preventDefault();
}
return
}
callback('post-before-fetch');
fetch(remote,
{
mode:"cors",
headers: {
'Access-Control-Allow-Origin':'*',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: "POST",
// use real location
body: JSON.stringify({ "email": mail,
"content": comment,
"location": window.location.pathname,
"replied_to": null // TODO: future feature: replies?
})
})
.then(async function(response){
let result = await response.json();
callback(result['status']);
})
.catch(function(exc){
console.log(exc);
callback('post-fetch-exception');
})
// Don't reload the page
if (btn) {
btn.preventDefault();
}
}

44
labertasche.yaml

@ -0,0 +1,44 @@
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
system:
web_url: "http://dev.localhost:1314/" # Url where the comment system is served
blog_url: "http://dev.localhost:1313/" # Url of your website
cookie-domain: "dev.localhost" # Url where the comment system is served
database_uri: "sqlite:///db/labertasche.db" # Database URI. See documentation. Default is sqlite.
secret: "6Gxvb52bIJCm2vfDsmWKzShKp1omrzVG" # CHANGE ME! THIS IS IMPORTANT!
output: "../../web/tuxstash.de/data/" # Base path for the output json
debug: false # Leave this as is, this is for development.
send_otp_to_publish: true # Disables confirmation w/ OTP via mail
gravatar:
cache: true # Enable caching of gravatar images
static_dir: "../../web/tuxstash.de/static/images/gravatar/" # Where to store cached images
size: 256 # only applies if images are cached,
# otherwise use ?s=size at the end of the gravatar url
dashboard:
username: "admin" # CHANGE ME!
password: "admin" # CHANGE ME!
addons:
smileys: true # Enable smiley replacements, set to false if unwanted
# https://www.w3schools.com/charsets/ref_emoji_smileys.asp
smileys:
":)": "&#128512"
":d": "&#128513"
":D": "&#128513"
";)": "&#128521"
":p": "&#128523"
":P": "&#128523"
":8": "&#128526"
"(:": "&#128579"
"$)": "&#129297"
":o": "&#128562"
":O": "&#128562"

26
labertasche/__init__.py

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from labertasche import (
models,
database,
blueprints,
helper,
mail,
settings
)
_all_ = [
models,
database,
blueprints,
helper,
mail,
settings
]

12
labertasche/blueprints/__init__.py

@ -0,0 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from .bp_comments import bp_comments
from .bp_login import bp_login
from .bp_dashboard import bp_dashboard

189
labertasche/blueprints/bp_comments.py

@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
import re
from sys import stderr
from antispam import is_spam as spam, score
from flask import Blueprint, jsonify, request, make_response, redirect
from flask_cors import cross_origin
from sqlalchemy import exc
from labertasche.database import labertasche_db as db
from labertasche.helper import is_valid_json, default_timestamp, check_gravatar, export_location
from labertasche.mail import mail
from labertasche.models import TComments, TLocation, TEmail
from labertasche.settings import Settings
from secrets import compare_digest
# Blueprint
bp_comments = Blueprint("bp_comments", __name__, url_prefix='/comments')
# Route for adding new comments
@bp_comments.route("/new", methods=['POST'])
@cross_origin()
def check_and_insert_new_comment():
if request.method == 'POST':
settings = Settings()
smileys = settings.smileys
addons = settings.addons
sender = mail()
# Check length of content and abort if too long or too short
if request.content_length > 2048:
return make_response(jsonify(status="post-max-length"), 400)
if request.content_length <= 0:
return make_response(jsonify(status="post-min-length"), 400)
# get json from request
new_comment = request.json
# save and sanitize location, nice try, bitch
location = new_comment['location'].strip().replace('.', '')
# Validate json and check length again
if not is_valid_json(new_comment) or \
len(new_comment['content']) < 40 or \
len(new_comment['email']) < 5:
print("too short", file=stderr)
return make_response(jsonify(status='post-invalid-json'), 400)
# Strip any HTML from message body
tags = re.compile('<.*?>')
special = re.compile('[&].*[;]')
content = re.sub(tags, '', new_comment['content']).strip()
content = re.sub(special, '', content).strip()
# Convert smileys
if addons['smileys']:
for key, value in smileys.items():
content = content.replace(key, value)
# Update values
new_comment.update({"content": content})
new_comment.update({"email": new_comment['email'].strip()})
new_comment.update({"location": location})
new_comment.update({"replied_to": None}) # Not (yet?) implemented
# Check mail
if not sender.validate(new_comment['email']):
return make_response(jsonify(status='post-invalid-email'), 400)
# check for spam
is_spam = spam(new_comment['content'])
has_score = score(new_comment['content'])
# Insert mail into spam if detected, allow if listed as such
email = db.session.query(TEmail).filter(TEmail.email == new_comment['email']).first()
if not email:
if is_spam:
entry = {
"email": new_comment['email'],
"is_blocked": True,
"is_allowed": False
}
db.session.add(TEmail(**entry))
if email:
if not email.is_allowed:
is_spam = True
if email.is_allowed:
is_spam = False
# Look for location
loc_query = db.session.query(TLocation)\
.filter(TLocation.location == new_comment['location'])
if loc_query.first():
# Set existing location id
new_comment.update({'location_id': loc_query.first().id_location})
# TComments does not have this field
new_comment.pop("location")
else:
# Insert new location
loc_table = {
'location': new_comment['location']
}
new_loc = TLocation(**loc_table)
db.session.add(new_loc)
db.session.flush()
db.session.refresh(new_loc)
new_comment.update({'location_id': new_loc.id_location})
# TComments does not have this field
new_comment.pop("location")
# insert comment
try:
new_comment.update({"is_published": False})
new_comment.update({"created_on": default_timestamp()})
new_comment.update({"is_spam": is_spam})
new_comment.update({"spam_score": has_score})
new_comment.update({"gravatar": check_gravatar(new_comment['email'])})
t_comment = TComments(**new_comment)
db.session.add(t_comment)
db.session.commit()
db.session.flush()
db.session.refresh(t_comment)
# Send confirmation link and store returned value
hashes = sender.send_confirmation_link(new_comment['email'])
setattr(t_comment, "confirmation", hashes[0])
setattr(t_comment, "deletion", hashes[1])
db.session.commit()
except exc.IntegrityError as e:
# Comment body exists, because content is unique
print(f"Duplicate from {request.environ['REMOTE_ADDR']}, error is:\n{e}", file=stderr)
return make_response(jsonify(status="post-duplicate"), 400)
except Exception as e: # must be at bottom
# mail(f"check_and_insert_new_comment has thrown an error: {e}", )
print(e, file=stderr)
return make_response(jsonify(status="post-internal-server-error"), 400)
export_location(location)
return make_response(jsonify(status="post-success", comment_id=t_comment.comments_id), 200)
# Route for confirming comments
@bp_comments.route("/confirm/<email_hash>", methods=['GET'])
@cross_origin()
def check_confirmation_link(email_hash):
settings = Settings()
comment = db.session.query(TComments).filter(TComments.confirmation == email_hash).first()
if comment:
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
if compare_digest(comment.confirmation, email_hash):
comment.confirmation = None
if not comment.is_spam:
setattr(comment, "is_published", True)
db.session.commit()
url = f"{settings.system['blog_url']}{location.location}#comment_{comment.comments_id}"
export_location(location.location)
return redirect(url)
return redirect(f"{settings.system['blog_url']}?cnf=true")
# Route for deleting comments
@bp_comments.route("/delete/<email_hash>", methods=['GET'])
@cross_origin()
def check_deletion_link(email_hash):
settings = Settings()
query = db.session.query(TComments).filter(TComments.deletion == email_hash)
comment = query.first()
if comment:
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
if compare_digest(comment.deletion, email_hash):
query.delete()
db.session.commit()
url = f"{settings.system['blog_url']}?deleted=true"
export_location(location.location)
return redirect(url)
return redirect(f"{settings.system['blog_url']}?cnf=true")

201
labertasche/blueprints/bp_dashboard.py

@ -0,0 +1,201 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from flask import Blueprint, render_template, request, redirect
from flask_login import login_required
from labertasche.database import labertasche_db as db
from labertasche.models import TLocation, TComments, TEmail
from labertasche.helper import dates_of_the_week
from sqlalchemy import func
import re
# Blueprint
bp_dashboard = Blueprint("bp_dashboard", __name__, url_prefix='/dashboard')
@bp_dashboard.route('/')
@login_required
def dashboard_index():
dates = dates_of_the_week()
spam = list()
published = list()
unpublished = list()
for each in dates:
spam_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date())\
.filter(TComments.is_spam == True).all()
pub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
.filter(TComments.is_spam == False)\
.filter(TComments.is_published == True).all()
unpub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
.filter(TComments.is_spam == False)\
.filter(TComments.is_published == False).all()
published.append(len(pub_comments))
spam.append(len(spam_comments))
unpublished.append(len(unpub_comments))
return render_template('dashboard.html', dates=dates, spam=spam, published=published, unpublished=unpublished)
@bp_dashboard.route('/review-spam/', methods=["POST", "GET"])
@bp_dashboard.route('/review-spam/<int:location>', methods=["POST", "GET"])
@login_required
def dashboard_review_spam(location=None):
all_locations = db.session.query(TLocation).all()
# Check post
if request.method == "POST":
location = request.form.get('selected_location')
# no parameters found
if location is None:
return render_template("review-spam.html", locations=all_locations, selected=location)
try:
if int(location) >= 1:
spam_comments = db.session.query(TComments).filter(TComments.location_id == location)\
.filter(TComments.is_spam == True)
return render_template("review-spam.html", locations=all_locations, selected=location,
spam_comments=spam_comments)
except ValueError:
pass
return render_template("review-spam.html", locations=all_locations, selected=location)
@bp_dashboard.route('/manage-comments/', methods=["POST", "GET"])
@bp_dashboard.route('/manage-comments/<int:location>', methods=["POST", "GET"])
@login_required
def dashboard_manage_regular_comments(location=None):
all_locations = db.session.query(TLocation).all()
# Check post
if request.method == "POST":
location = request.form.get('selected_location')
# no parameters found
if location is None:
return render_template("manage-comments.html", locations=all_locations, selected=location)
try:
if int(location) >= 1:
spam_comments = db.session.query(TComments).filter(TComments.location_id == location) \
.filter(TComments.is_spam == False)
return render_template("manage-comments.html", locations=all_locations, selected=location,
spam_comments=spam_comments)
except ValueError:
pass
return render_template("manage-comments.html", locations=all_locations, selected=location)
@bp_dashboard.route('/manage-mail/')
@login_required
def dashboard_allow_email():
addresses = db.session.query(TEmail).all()
return render_template("manage_mail_addresses.html", addresses=addresses)
@bp_dashboard.route('/toggle-mail-allowed/<int:id_email>')
@login_required
def dashboard_allow_email_toggle(id_email):
address = db.session.query(TEmail).filter(TEmail.id_email == id_email).first()
if address:
setattr(address, "is_allowed", (not address.is_allowed))
setattr(address, "is_blocked", (not address.is_blocked))
db.session.commit()
return redirect(request.referrer)
@bp_dashboard.route('/reset-mail-reputation/<int:id_email>')
@login_required
def dashboard_reset_mail_reputation(id_email):
db.session.query(TEmail).filter(TEmail.id_email == id_email).delete()
db.session.commit()
return redirect(request.referrer)
@bp_dashboard.route('/delete-comment/<int:location_id>/<int:comment_id>', methods=['GET'])
@login_required
def dashboard_review_spam_delete_comment(location_id, comment_id):
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
db.session.delete(comment)
db.session.commit()
# Remove after last slash, to keep the location but get rid of the comment id
url = re.match("^(.*[/])", request.referrer)[0]
return redirect(f"{url}/{location_id}")
@bp_dashboard.route('/allow-comment/<int:location_id>/<int:comment_id>', methods=['GET'])
@login_required
def dashboard_review_spam_allow_comment(comment_id, location_id):
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
if comment:
setattr(comment, 'is_published', True)
setattr(comment, 'is_spam', False)
db.session.commit()
url = re.match("^(.*[/])", request.referrer)[0]
return redirect(f"{url}/{location_id}")
@bp_dashboard.route('/block-mail/<int:location_id>/<int:comment_id>', methods=["GET"])
@login_required
def dashboard_review_spam_block_mail(location_id, comment_id):
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
if comment:
mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
if mail:
setattr(mail, 'is_allowed', False)
setattr(mail, 'is_blocked', True)
else:
new_mail = {
"email": comment.first().email,
"is_allowed": False,
"is_blocked": True
}
db.session.add(TEmail(**new_mail))
# Delete all comments made by this mail address
db.session.query(TComments).filter(TComments.email == comment.email).delete()
db.session.commit()
url = re.match("^(.*[/])", request.referrer)[0]
return redirect(f"{url}/{location_id}")
@bp_dashboard.route('/allow-user/<int:location_id>/<int:comment_id>', methods=["GET"])
@login_required
def dashboard_review_spam_allow_user(location_id, comment_id):
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
if comment:
mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
if mail:
setattr(mail, 'is_allowed', True)
setattr(mail, 'is_blocked', False)
else:
new_mail = {
"email": comment.email,
"is_allowed": True,
"is_blocked": False
}
db.session.add(TEmail(**new_mail))
# Allow all comments made by this mail address
all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all()
if all_comments:
for comment in all_comments:
setattr(comment, 'is_published', True)
setattr(comment, 'is_spam', False)
db.session.commit()
url = re.match("^(.*[/])", request.referrer)[0]
return redirect(f"{url}/{location_id}")

46
labertasche/blueprints/bp_login.py

@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from flask import Blueprint, render_template, request, redirect, url_for
from flask_cors import cross_origin
from labertasche.helper import check_auth, User
from flask_login import login_user, current_user, logout_user
# Blueprint
bp_login = Blueprint("bp_login", __name__)
@cross_origin()
@bp_login.route('/', methods=['GET'])
def show_login():
if current_user.is_authenticated:
return redirect(url_for('bp_dashboard.dashboard_index'))
return render_template('login.html')
@cross_origin()
@bp_login.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if check_auth(username, password):
login_user(User(0), remember=True)
return redirect(url_for('bp_dashboard.dashboard_index'))
# Redirect get request to the login page
return redirect(url_for('bp_login.show_login'))
@cross_origin()
@bp_login.route('/logout/', methods=["GET"])
def logout():
if current_user.is_authenticated:
logout_user()
return redirect(url_for("bp_login.show_login"))

12
labertasche/database/__init__.py

@ -0,0 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from flask_sqlalchemy import SQLAlchemy
# Create SQLAlchemy
labertasche_db = SQLAlchemy()

190
labertasche/helper/__init__.py

@ -0,0 +1,190 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
import datetime
import json
from labertasche.models import TLocation, TComments
from labertasche.settings import Settings
from labertasche.database import labertasche_db as db
from functools import wraps
from hashlib import md5
from flask import request
from flask_login import UserMixin
from secrets import compare_digest
from pathlib import Path
from sys import stderr
from re import match as re_match
import requests
class User(UserMixin):
def __init__(self, user_id):
self.id = user_id
def is_valid_json(j):
"""
Tries to load the json to test if it is valid.
:param j: The json to test.
:return: True if the json is valid, False on any exception.
"""
try:
json.dumps(j)
return True
except json.JSONDecodeError as e:
print("not valid json")
return False
def default_timestamp():
"""Timestamp used by the project to ensure consistency"""
date = datetime.datetime.now().replace(microsecond=0)
return date
def time_to_js(obj):
""""
Returns a timestring readable by Javascript
"""
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.isoformat()
def alchemy_query_to_dict(obj):
"""
Used when exporting the data. It truncates the mail, removes the T from the date string, etc.
:param obj: A single query item from sqlalchemy.
:return: a dict with the query
"""
no_mail = re_match("^.*[@]", obj.email)[0]
result = {
"comment_id": obj.comments_id,
"email": no_mail,
"content": obj.content,
"created_on": time_to_js(obj.created_on).replace("T", " "),
"replied_to": obj.replied_to,
"gravatar": obj.gravatar
}
return dict(result)
# Come on, it's a mail hash, don't complain
# noinspection InsecureHash
def check_gravatar(email: str):
"""
Builds the gravatar email hash, which uses md5.
You may use ?size=128 for example to dictate size in the final template.
:param email: the email to use for the hash
:return: the gravatar url of the image
"""
settings = Settings()
options = settings.gravatar
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
if options['cache']:
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={options['size']}"
response = requests.get(url)
if response.ok:
outfile = Path(f"{options['static_dir']}/{gravatar_hash}.jpg")
if not outfile.exists():
with outfile.open('wb') as fp:
response.raw.decode_content = True
for chunk in response:
fp.write(chunk)
return gravatar_hash
def check_auth(username: str, password: str):
"""
Compares username and password from the settings file in a safe way.
Direct string comparison is vulnerable to timing attacks
https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
:param username: username entered by the user
:param password: password entered by the user
:return: True if equal, False if not
"""
settings = Settings()
if compare_digest(username, settings.dashboard['username']) and \
compare_digest(password, settings.dashboard['password']):
return True
return False
def basic_login_required(f):
"""
Decorator for basic auth
"""
@wraps(f)
def wrapped_view(**kwargs):
auth = request.authorization
if not (auth and check_auth(auth.username, auth.password)):
return ('Unauthorized', 401, {
'WWW-Authenticate': 'Basic realm="Login Required"'
})
return f(**kwargs)
return wrapped_view
def export_location(location: str) -> bool:
"""
Exports the comments for the location after the comment was accepted
:param location: relative url of the hugo page
"""
try:
# Query
loc_query = db.session.query(TLocation).filter(TLocation.location == location).first()
if loc_query:
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
.filter(TComments.is_published == True) \
.filter(TComments.location_id == loc_query.id_location) \
.filter(TComments.replied_to == None)
bundle = {
"comments": []
}
for comment in comments:
bundle['comments'].append(alchemy_query_to_dict(comment))
path_loc = re_match(".*(?=/)", loc_query.location)[0]
system = Settings().system
out = Path(f"{system['output']}/{path_loc}.json")
out = out.absolute()
print(out)
folder = out.parents[0]
folder.mkdir(parents=True, exist_ok=True)
with out.open('w') as fp:
json.dump(bundle, fp)
return True
except Exception as e:
# mail(f"export_comments has thrown an error: {str(e)}")
print(e, file=stderr)
return False
def dates_of_the_week():
"""
Finds all dates of this week and returns them as a list,
going from midnight on monday to sunday 1 second before midnight
:return: A list containing the dates
"""
date_list = list()
now = datetime.datetime.now()
monday = now - datetime.timedelta(days=now.weekday(), hours=now.hour, minutes=now.minute, seconds=now.second,
microseconds=now.microsecond)
date_list.append(monday)
for each in range(1, 6):
monday = monday + datetime.timedelta(days=1)
date_list.append(monday)
date_list.append((monday + datetime.timedelta(days=1, hours=23, minutes=59, seconds=59)))
return date_list

100
labertasche/mail/__init__.py

@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from json import load as j_load
from pathlib import Path
from platform import system
from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException
from ssl import create_default_context
from labertasche.settings import Settings
from validate_email import validate_email
from secrets import token_urlsafe
class mail:
def __init__(self):
path = Path("/etc/labertasche/mail_credentials.json")
if system().lower() == "windows":
path = Path("mail_credentials.json")
with path.open("r") as fp:
self.credentials = j_load(fp)
def send(self, txt_what: str, html_what: str, to: str):
if not self.credentials['enable']:
return
txtmail = MIMEText(txt_what, "plain", _charset='utf8')
multimime = MIMEMultipart('alternative')
multimime['Subject'] = "Comment confirmation pending"
multimime['From'] = self.credentials['email-sendfrom']
multimime['To'] = to
multimime.attach(txtmail)
# Only send HTML if needed
if html_what is not None:
htmlmail = MIMEText(html_what, "html", _charset='utf8')
multimime.attach(htmlmail)
try:
with SMTP_SSL(host=self.credentials['smtp-server'],
port=self.credentials['smtp-port'],
context=create_default_context()) as server:
server.login(user=self.credentials['email-user'], password=self.credentials['email-password'])
server.sendmail(to_addrs=to,
msg=multimime.as_string(),
from_addr=self.credentials['email-sendfrom'])
except SMTPHeloError as helo:
print(f"SMTPHeloError: {helo}")
except SMTPAuthenticationError as auth_error:
print(f"Authentication Error: {auth_error}")
except SMTPException as e:
print(f"SMTPException: {e}")
def send_confirmation_link(self, email):
"""
Send confirmation link after entering a comment
:param email: The address to send the mail to
:return: A tuple with the confirmation token and the deletion token, in this order
"""
settings = Settings()
confirm_digest = token_urlsafe(48)
delete_digest = token_urlsafe(48)
confirm_url = f"{settings.system['web_url']}/comments/confirm/{confirm_digest}"
delete_url = f"{settings.system['web_url']}/comments/delete/{delete_digest}"
txt_what = f"Hey there. You have made a comment on {settings.system['blog_url']}. Please confirm it by " \
f"copying this link into your browser:\n{confirm_url}\nIf you want to delete your comment for,"\
f"whatever reason, please use this link:\n{delete_url}"
html_what = f"Hey there. You have made a comment on {settings.system['blog_url']}.<br>Please confirm it by " \
f"clicking on this <a href='{confirm_url}'>link</a>.<br>"\
f"In case you want to delete your comment later, please click <a href='{delete_url}'>here</a>."\
f"<br><br>If you think this is in error or someone made this comment in your name, please "\
f"write me a <a href='mailto:contact@tuxstash.de'>mail</a> to discuss options such as " \
f"blocking your mail from being used."
self.send(txt_what, html_what, email)
return confirm_digest, delete_digest
def validate(self, addr):
# validate email
is_valid = validate_email(email_address=addr,
check_regex=True,
check_mx=False,
dns_timeout=10,
use_blacklist=True,
debug=False)
return is_valid

11
labertasche/models/__init__.py

@ -0,0 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from .t_comments import TComments
from .t_location import TLocation
from .t_emails import TEmail

34
labertasche/models/t_comments.py

@ -0,0 +1,34 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from labertasche.database import labertasche_db as db
from sqlalchemy import ForeignKey
class TComments(db.Model):
# table name
__tablename__ = "t_comments"
__table_args__ = {'useexisting': True}
# primary key
comments_id = db.Column(db.Integer, primary_key=True)
# foreign keys
location_id = db.Column(db.Text, ForeignKey('t_location.id_location'), nullable=False)
# data
email = db.Column(db.Text, nullable=False)
content = db.Column(db.Text, nullable=False, unique=True)
created_on = db.Column(db.DateTime, nullable=False)
is_published = db.Column(db.Boolean, nullable=False)
is_spam = db.Column(db.Boolean, nullable=False)
spam_score = db.Column(db.Float, nullable=False)
replied_to = db.Column(db.Boolean, nullable=True)
confirmation = db.Column(db.Text, nullable=True)
deletion = db.Column(db.Text, nullable=True)
gravatar = db.Column(db.Text, nullable=True)

23
labertasche/models/t_emails.py

@ -0,0 +1,23 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from labertasche.database import labertasche_db as db
class TEmail(db.Model):
# Table name
__tablename__ = 't_email'
__table_args__ = {'useexisting': True}
# primary key
id_email = db.Column(db.Integer, primary_key=True)
# data
email = db.Column(db.Integer, unique=True)
is_blocked = db.Column(db.Boolean)
is_allowed = db.Column(db.Boolean)

21
labertasche/models/t_location.py

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
from labertasche.database import labertasche_db as db
class TLocation(db.Model):
# table name
__tablename__ = "t_location"
__table_args__ = {'useexisting': True}
# primary key
id_location = db.Column(db.Integer, primary_key=True)
# data
location = db.Column(db.Text, nullable=False, unique=True)

30
labertasche/settings/__init__.py

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# /**********************************************************************************
# * _author : Domeniko Gentner
# * _mail : code@tuxstash.de
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
# * _license : This project is under MIT License
# *********************************************************************************/
import yaml
from pathlib import Path
from platform import system
class Settings:
"""
Automatically loads the settings from /etc/ on Linux and same directory on other OS
"""
def __init__(self):
file = Path("labertasche.yaml")
if system().lower() == "linux":
file = Path("/etc/labertasche/labertasche.yaml")
with file.open('r') as fp:
conf = yaml.safe_load(fp)
self.system = conf['system']
self.dashboard = conf['dashboard']
self.gravatar = conf['gravatar']
self.addons = conf['addons']
self.smileys = conf['smileys']

9
mail_credentials.json

@ -0,0 +1,9 @@
{
"enable": true,
"smtp-server": "mail server",
"smtp-port": 465,
"email-user": "username for smtp",