commit
9ac6e9930e
106 changed files with 17627 additions and 0 deletions
@ -0,0 +1,8 @@ |
||||
.idea |
||||
__pycache__/ |
||||
venv |
||||
db/labertasche.db-shm |
||||
db/labertasche.db-wal |
||||
output |
||||
/output/ |
||||
*.sql |
@ -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. |
||||
|
@ -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" |
@ -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": {} |
||||
} |
@ -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: |
||||
|
||||
 |
||||
|
||||
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,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> |
||||
``` |
@ -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> |
After Width: | Height: | Size: 53 KiB |
@ -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(); |
||||
} |
||||
} |
@ -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: |
||||
":)": "😀" |
||||
":d": "😁" |
||||
":D": "😁" |
||||
";)": "😉" |
||||
":p": "😋" |
||||
":P": "😋" |
||||
":8": "😎" |
||||
"(:": "🙃" |
||||
"$)": "🤑" |
||||
":o": "😲" |
||||
":O": "😲" |
||||
|
@ -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 |
||||
] |
@ -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 |
||||
|
@ -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") |
@ -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}") |
@ -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")) |
@ -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() |
@ -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 |
@ -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}") |
||||