177 Commits

Author SHA1 Message Date
yax 9cc1b62a5c 2.0 beta 4 2021-02-03 18:34:41 +01:00
yax 96b821d01a Merge branch 'master' of github.com:kianby/stacosys 2021-02-03 18:32:41 +01:00
yax af9bfcdddc add global count 2021-02-03 18:31:46 +01:00
yax 0056feaa7f upgrade dependencies 2021-02-03 18:31:35 +01:00
yax b8da995cd9 Merge pull request #4 from kianby/develop
markdown joke
2021-01-31 17:08:05 +01:00
yax b290fe35d3 markdown joke 2021-01-31 17:06:48 +01:00
yax 287a441f40 Merge pull request #3 from kianby/develop
update README, licence GPL v2 upgraded to v3
2021-01-31 17:03:07 +01:00
yax a5bfc88554 update README, licence GPL v2 upgraded to v3 2021-01-31 17:01:26 +01:00
yax 3a19db0b2d fix a couple of bugs 2021-01-28 19:00:54 +01:00
yax 7070e2a273 upgrade libs and follow linter recommandations 2021-01-24 18:23:20 +01:00
yax d6f7cdabfe version 2.0 beta 2 2021-01-23 18:06:41 +01:00
yax f52eae82ab python 3.9 required 2021-01-23 18:04:52 +01:00
yax e2ce3fa797 version 2.0-b1 2021-01-20 19:40:21 +01:00
yax b5992560e8 ignore sqlite files 2021-01-19 19:32:58 +01:00
yax 03e5d2dcea add mypy tool 2021-01-19 19:27:49 +01:00
yax af414d11e1 stop using typing module. Python 3.9 required. 2021-01-19 19:01:20 +01:00
yax 3d818ad417 fix flask parameter retrieval 2021-01-19 19:00:57 +01:00
yax 93a4971d79 major version 2.0 2021-01-19 19:00:35 +01:00
yax 450280e9b9 get rid of site token retrieved from DB 2021-01-19 18:59:57 +01:00
yax de6e9ba4f0 remove unused __init__.py files 2021-01-19 18:59:26 +01:00
yax e6d950fe56 move db migration tool 2021-01-19 18:58:59 +01:00
yax 7bd3c6ffef database migration script 2021-01-18 19:25:45 +01:00
yax 435cddc4e4 use enums 2021-01-14 18:45:06 +01:00
yax e6986e3587 remove site entity 2021-01-10 21:03:55 +01:00
yax e85e53a48e refactor config constants 2021-01-10 15:36:29 +01:00
yax cae77c2d05 connect new templater implementation 2021-01-10 14:26:59 +01:00
yax fde8c33de0 pytest templater 2021-01-09 19:15:58 +01:00
yax 23a45580ee unit test templater 2021-01-03 18:25:19 +01:00
yax b0867f0e44 no more used 2021-01-03 18:24:34 +01:00
yax 5fea1e358a improve encapsulation 2021-01-03 18:24:16 +01:00
yax 962e0d2e41 object-oriented and DI 2021-01-02 12:36:42 +01:00
yax ab54ab981a backup sqlite db to json with tinyDB 2021-01-01 11:18:35 +01:00
yax 9b578637ba config becomes object-oriented and pytests have been added 2021-01-01 11:17:09 +01:00
yax 5bf6040d2c git ignore 2020-12-28 19:44:26 +01:00
yax 674811b77f draft tinydb persistence 2020-12-28 19:43:23 +01:00
yax 3293a10abe tinydb migration 2020-12-27 20:04:20 +01:00
yax eaad39b05b move package root 2020-12-27 18:37:07 +01:00
yax 7f6416c41a add type checking 2020-03-31 20:20:07 +02:00
yax 21ab9c7e8b fix plain text part content 2020-03-31 20:15:29 +02:00
yax ea7585f078 fix named tuple 2020-03-31 20:09:38 +02:00
yax e95c6264a0 add type checking 2020-03-31 19:53:59 +02:00
yax f1c5d83495 fix var 2020-03-29 16:05:12 +02:00
yax 7531969627 fix fn name 2020-03-29 15:57:25 +02:00
yax b6da0405fc manage imap/smtp directly. remove srmail dependency 2020-03-29 15:43:13 +02:00
yax 09f6e1a250 python3 2020-03-28 18:01:52 +01:00
yax 63c487e548 clean-up code 2020-03-28 17:57:13 +01:00
yax d31bf56096 fix pyproject readme 2019-12-28 17:19:45 +01:00
yax 5261a325f3 Stacosys 1.1.0 - Poetry 2019-12-28 17:02:25 +01:00
yax 5fab9cae2f anti-spam 2019-09-01 15:50:05 +02:00
yax c2f2e9ab89 trace 2019-09-01 15:30:50 +02:00
yax 85fe290e83 captcha 2019-08-31 17:16:34 +02:00
yax 1c75ecc9a1 update requirements 2019-08-25 18:37:41 +02:00
yax 9f4d778eb2 argparse 2018-10-27 18:10:28 +02:00
yax 21dbce7488 logging 2018-09-30 18:24:25 +02:00
yax 5289cc3698 config 2018-09-15 19:03:57 +02:00
yax ec86069ec7 microservicified 2018-09-15 18:15:21 +02:00
yax 8311053d08 WIP 2018-09-15 15:50:25 +02:00
yax 6867e71d7c WIP 2018-09-15 15:02:06 +02:00
yax 3c4a25e5ad WIP 2018-09-15 13:38:03 +02:00
yax 637b00261a upgrade libs 2018-09-02 11:59:53 +02:00
yax a06db608bc minimalism 2018-09-02 11:42:24 +02:00
yax 83cd8725c3 rabbit connection 2018-07-14 12:42:59 +02:00
yax 6874f4d5ca rabbitmq util 2018-07-13 19:03:46 +02:00
yax 281c10ec3f revert dummy change 2018-07-11 18:53:07 +02:00
yax 8e27669e03 revert dummy change 2018-07-11 18:46:04 +02:00
yax d1e924d638 system exit on comment approval 2018-07-10 18:55:40 +02:00
yax afd9aea763 update default config 2018-06-17 15:30:25 +02:00
yax e64d4035a0 update default config 2018-06-17 15:25:27 +02:00
yax ad8c1cf115 Enforce email check 2018-02-11 14:48:48 +01:00
yax 3064dca6ca add ping method 2018-02-04 17:44:36 +01:00
yax 6cd8341a2f fix 2018-01-28 18:22:38 +01:00
yax 20bcb896bc fix anti-spam 2018-01-28 18:20:42 +01:00
yax 03b411e433 anti-spam 2018-01-28 18:12:53 +01:00
yax 18b879be14 log http headers 2018-01-28 17:53:10 +01:00
yax 68408d0372 share rabbitmq conn 2018-01-27 17:07:07 +01:00
yax e0c9f335fc use rabbitmq 2018-01-27 16:31:08 +01:00
yax feb280ed8c Finalize ZMQ interface 2018-01-20 11:56:14 +01:00
yax cedac41b10 on microservice way 2018-01-20 10:03:41 +01:00
yax 8ebed3cda6 Configuration moved to JSON and validated by JSON Schema 2018-01-20 08:57:49 +01:00
yax 47bda266a4 fetch emails through Zero MQ 2018-01-16 18:49:19 +01:00
yax 8c5d4d7301 Fix import 2017-12-03 13:57:15 +01:00
yax e9ad960971 Fix import error 2017-12-03 13:51:34 +01:00
yax f05bfb8383 add private mode 2017-12-03 13:40:24 +01:00
yax 3e15b83d2a Undo 2017-11-18 18:38:54 +01:00
yax d7f8933f77 rollback redirect URL change 2017-11-18 18:36:37 +01:00
yax f2581445d2 Fix redirect 2017-11-18 18:27:39 +01:00
yax 5316a3771b upd config 2017-11-12 18:20:05 +01:00
yax 26808631ac redirect 2017-11-11 08:44:28 +01:00
yax 04ce4dfbb7 Add HTML form entry point 2017-11-11 07:33:21 +01:00
yax 885996f25c remove traces 2017-07-16 18:12:39 +02:00
yax 7deccf7425 back to flask :-) 2017-07-16 17:35:38 +02:00
yax 7cbcb0b5cb Fix URL 2017-07-16 17:19:56 +02:00
yax 7a8e086073 Finalize http go 2017-07-16 16:32:16 +02:00
yax d723c2bbd3 Draft Go HTTP server 2017-07-16 13:26:36 +02:00
yax d740801eed Cache time to 120s 2017-07-09 17:18:30 +02:00
yax c5ae33f738 one cache per worker 2017-07-09 17:13:41 +02:00
yax c6cf9ac795 homemade caching function 2017-07-09 16:53:58 +02:00
yax 39e547c303 Fix count caching 2017-07-09 15:56:17 +02:00
yax f7bd755f33 Fix count caching 2017-07-09 15:55:40 +02:00
yax 1933452338 cache count 2017-07-09 15:27:59 +02:00
yax 15ebe08bd5 cors change 2017-07-09 14:25:19 +02:00
yax 537940381b Debug post options 2017-07-09 11:42:36 +02:00
yax c0666262be Fix post 2017-07-09 11:39:12 +02:00
yax ae195d9826 sanic compliance 2017-07-09 11:27:16 +02:00
yax 7bbe99852d configure workers 2017-07-08 20:42:58 +02:00
yax 0f55ec361f cors 2017-07-08 20:31:40 +02:00
yax f00db85541 replace flask with sanic 2017-07-08 20:21:39 +02:00
yax 17c302f999 flask cache 2017-07-08 18:53:49 +02:00
yax 657d5400f8 wsgi fix 2017-07-08 16:39:17 +02:00
yax 6d61e7c41f Ready for uwsgi 2017-07-07 19:42:10 +02:00
yax 6a1dbeab90 Fix RSS guid 2016-11-14 14:34:52 +01:00
yax db9c5437dd do not overwrite RSS file on app launch 2016-11-08 13:51:26 +01:00
yax a590d7c72c Catch exception on email reply processing. Issue #1 2016-05-09 13:23:47 +02:00
yax f90af4fbad Improve debug logging level 2016-05-09 13:12:14 +02:00
yax f2d95da063 Improve debug logging level 2016-05-09 13:10:11 +02:00
yax 7049e9e905 Set Flask logging level 2016-05-09 13:05:01 +02:00
yax 5848b8df8e Update README.md 2015-10-25 14:52:27 +01:00
yax 41911a7589 Update README.md 2015-10-25 14:40:24 +01:00
yax 5c894d7957 Fix report 2015-10-09 20:45:44 +02:00
yax 167c84645b Fix RSS links 2015-10-05 16:40:39 +02:00
yax 0ef2280564 Change RSS title 2015-09-28 09:11:28 +02:00
yax 90a117ef9a Enhance gitignore 2015-09-27 20:01:49 +02:00
yax 37095bbe79 Generate RSS for comments 2015-09-27 19:40:40 +02:00
yax 6237b30e6f Use PyRSS2Gen 2015-09-27 12:04:29 +02:00
yax aa11b1f1c2 Fix typo 2015-09-26 16:05:34 +02:00
yax 08bd67f17f Delete report data once generated 2015-09-20 12:42:15 +02:00
yax 2b08f01e8f Fix log format 2015-09-20 12:39:09 +02:00
yax b9722ffd47 Fix QP retrieval 2015-09-20 12:36:06 +02:00
yax 4b71710dc8 Accept / reject comments via API 2015-09-20 12:33:42 +02:00
yax c149a44d6d Enable email sending 2015-09-20 12:20:28 +02:00
yax d40cef9a25 Improve reporting 2015-09-20 12:15:41 +02:00
yax beb84a70f4 Unleash report name constraint 2015-09-11 21:42:50 +02:00
yax 0a153e72fb Fix typo 2015-09-11 21:36:35 +02:00
yax fc438a8ed2 Fix typo 2015-09-11 21:28:09 +02:00
yax 11ab7b67c3 Use absolute URL 2015-09-11 21:27:06 +02:00
yax 5c934e51fd Use absolute URL in emails 2015-09-11 21:21:35 +02:00
yax 18ad5870b7 Add report feature 2015-09-11 20:23:53 +02:00
yax 49c4083882 Draft reporting 2015-09-06 18:58:07 +02:00
yax 890e18e9d5 Strip comment attributes 2015-09-05 15:57:45 +02:00
yax d4dd8ac307 Fix comment subscription 2015-09-04 13:26:55 +02:00
yax 1782537006 Fix comment subscription 2015-09-04 13:03:41 +02:00
yax cbf47e7f78 Fix comment subscription 2015-09-04 12:58:59 +02:00
yax b2f9cb596f Fix typo 2015-08-05 11:16:38 +02:00
yax 38e632e938 Use windows.location.pathname as page id 2015-05-26 22:27:13 +02:00
yax e19aae121b Manage reader subscription 2015-05-24 19:40:46 +02:00
yax ffab88e369 Update e-mail templates 2015-05-24 19:18:36 +02:00
yax 6db8fac605 PEP8 compliance 2015-05-18 13:23:24 +02:00
yax c9307935db Remove chardet dependency 2015-05-18 13:22:49 +02:00
yax ecda6c3217 Change admin email subject 2015-05-17 19:26:19 +02:00
yax 019c78f2a1 Finalize demo redirect page 2015-05-17 19:20:24 +02:00
yax 212d5a9442 Complete publishing process by email 2015-05-16 20:55:55 +02:00
yax 893e096ce6 Set comment id into email subject 2015-05-16 18:23:40 +02:00
yax 91344ab775 Send comment by email via SRMail 2015-05-16 18:12:18 +02:00
yax 99b2eb3628 Connect Stacosys to SRMail 2015-05-16 10:59:44 +02:00
yax a525c4de19 Merge pecosys work around comment processing with stacosys 2015-05-15 19:51:02 +02:00
yax 4ec6c58b08 Split cleanly stacosys API usage and page rendering
Progress on new comment post method
2015-05-12 22:30:10 +02:00
yax 538659c634 Submitting comment. Work in progress 2015-05-10 19:39:32 +02:00
yax deabd382be Draft new comment submitting 2015-05-10 12:44:04 +02:00
yax 0f57aa267c Split page rendering and CORS requests against stacosys 2015-05-09 20:46:48 +02:00
yax 20b01caf86 Add 'get count of comment' method
Draft JavaScript side
2015-05-09 20:25:35 +02:00
yax ac00f6ad33 Convert Markdown to HTML in JS 2015-05-09 16:28:33 +02:00
yax cf84a58577 Link demo blog with stacosys backend 2015-05-09 15:16:56 +02:00
yax 0548b50b73 Add CORS capability to Flask
Add static blog demo files
2015-05-09 12:51:14 +02:00
yax a571a78215 Fix SQLite date sorting 2015-05-06 19:46:51 +02:00
yax f29a53be94 SQLite compatibility 2015-05-04 20:01:47 +02:00
yax 2d29502e5f Finalize migration tool.
Retrieve comments by GET and POST
2015-05-03 18:31:15 +02:00
yax f9accbffef Add created datetime to Comment model 2015-05-02 20:29:08 +02:00
yax 6681a721a2 Progress on API to retrieve comments 2015-05-02 20:24:56 +02:00
yax a0bb9938fa Remove protocol part from URL 2015-05-02 20:24:12 +02:00
yax 9c49c842f6 Add first controller 2015-05-02 13:43:38 +02:00
yax 89cb8c0943 Add run.sh 2015-05-02 13:11:18 +02:00
yax e3fd4f20c9 Bootstrap Flask application 2015-05-02 13:08:07 +02:00
yax a116d5bc01 relative index is not useful. Peewee has paging capability built-in 2015-05-01 19:29:26 +02:00
yax 7441a80217 Conversion tool is near from complete. Use Peewee as ORM 2015-05-01 18:57:17 +02:00
yax 451bf1915e Change .gitignore 2015-04-29 20:26:00 +02:00
yax 66fa62d6be Migration tools from Pecosys to Stacosys 2015-04-29 20:20:07 +02:00
yax 4e2fca2937 Initial commit 2015-04-29 18:45:52 +02:00
60 changed files with 1642 additions and 2956 deletions
-25
View File
@@ -1,25 +0,0 @@
name: docker
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11.0"
- name: Install poetry
uses: abatilo/actions-poetry@v2
with:
poetry-version: 1.2.2
- name: Install dependencies
run: poetry install
- name: Build project
run: poetry build
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
-31
View File
@@ -1,31 +0,0 @@
name: pytest
on: push
jobs:
ci:
strategy:
fail-fast: false
matrix:
python-version: [3.11.0]
poetry-version: [1.2.2]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: poetry install
- name: Pytest and Coverage
run: |
poetry run coverage run -m --source=stacosys pytest tests
poetry run coverage report
- name: Send report to Coveralls
run: poetry run coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
-3
View File
@@ -70,6 +70,3 @@ stacosys/lib64
workspace.code-workspace
*.sqlite
config-server.ini
config-dev.ini
.idea/
.python-version
-622
View File
@@ -1,622 +0,0 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\' represents the directory delimiter on Windows systems, it
# can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=^\.#
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.11
# Discover python modules and packages in the file system subtree.
recursive=no
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=BaseException,
Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
missing-module-docstring,
missing-class-docstring,
missing-function-docstring,
too-few-public-methods
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-26
View File
@@ -1,26 +0,0 @@
FROM python:3.11.0-alpine
ARG STACOSYS_VERSION=3.3
ARG STACOSYS_FILENAME=stacosys-${STACOSYS_VERSION}-py3-none-any.whl
RUN apk update && apk add bash && apk add wget
# Timezone
RUN apk add tzdata
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
RUN echo "Europe/Paris" > /etc/timezone
# Clean apk cache
RUN rm -rf /var/cache/apk/*
COPY docker/docker-init.sh /usr/local/bin/
RUN chmod +x usr/local/bin/docker-init.sh
RUN cd /
COPY dist/${STACOSYS_FILENAME} /
#RUN wget https://github.com/kianby/stacosys/releases/download/${STACOSYS_VERSION}/${STACOSYS_FILENAME}
RUN python3 -m pip install ${STACOSYS_FILENAME} --target /stacosys
RUN rm -f ${STACOSYS_FILENAME}
WORKDIR /stacosys
CMD ["docker-init.sh"]
-16
View File
@@ -1,16 +0,0 @@
all: black test typehint lint
black:
poetry run isort --multi-line 3 --profile black stacosys/ tests/
poetry run black --target-version py311 stacosys/ tests/
test:
poetry run coverage run -m --source=stacosys pytest
poetry run coverage report
typehint:
poetry run mypy --ignore-missing-imports stacosys/ tests/
lint:
poetry run pylint stacosys/
+13 -15
View File
@@ -1,11 +1,8 @@
[![GitLicense](https://gitlicense.com/badge/kianby/stacosys)](https://gitlicense.com/license/kianby/stacosys)
[![Python version](https://img.shields.io/badge/Python-3.10-blue.svg)](https://www.python.org/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Flask version](https://img.shields.io/badge/Flask-2.1-green.svg)](https://flask.palletsprojects.com) [![Peewee version](https://img.shields.io/badge/Peewee-3.14-green.svg)](https://docs.peewee-orm.com/)
[![Build Status - pytest](https://github.com/kianby/stacosys/workflows/pytest/badge.svg)](https://github.com/kianby/stacosys) [![Coverage Status](https://coveralls.io/repos/github/kianby/stacosys/badge.svg?branch=main)](https://coveralls.io/github/kianby/stacosys?branch=main) [![Build status - docker image](https://github.com/kianby/stacosys/workflows/docker/badge.svg)](https://hub.docker.com/r/kianby/stacosys)
## Stacosys
Stacosys (aka STAtic blog COmment SYStem) is a fork of [Pecosys](http://github.com/kianby/pecosys) trying to fix Pecosys design drawbacks and to provide a basic alternative to comment hosting services like Disqus. Stacosys works with any static blog or even a simple HTML page.
Stacosys (aka STAtic blog COmment SYStem) is a fork of [Pecosys](http://github.com/kianby/pecosys) trying to fix Pecosys design drawbacks and to provide an humble alternative to comment hosting services like Disqus. Stacosys protects your readers's privacy.
Stacosys works with any static blog or even a simple HTML page. It uses e-mails to communicate with the blog administrator. It doesn't sound *hype* but I'm an old-school guy. E-mails are reliable and an universal way to communicate. You can answer from any device using an e-mail client.
### Features overview
@@ -14,24 +11,25 @@ Stacosys main feature is comment management.
Here is the workflow:
- Readers submit comments via a comment form embedded in blog pages
- Blog administrator receives an e-mail notification from Stacosys when a
- Blog administrator receives an email notification from Stacosys when a
comment is submitted
- Blog administrator can approve or drop the comment through a simple web admin interface
- Blog administrator can approve or drop the comment by replying to e-mail
- Stacosys stores approved comment in its database.
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is optionally requested in submission form to resolve gravatar id but never sent to Stacosys.
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is requested in submission form (but optional) to resolve gravatar id and it it not sent to stacosys.
Stacosys is more or less localized (english and french).
Stacosys is localized (english and french).
### Technically speaking, how does it work?
Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a request allows retrieving comments for a given page. Similarly, a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose an SMTP configuration is needed.
Stacosys can be hosted on the same server or on a different server than the blog. Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a simple request allows to retrieve comments for a given page. Similarly a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose a dedicated email is assigned to Stacosys.
### Little FAQ
*How do you block spammers?*
- Current comment form is basic: no captcha support but protected by a honeypot.
- Current comment form is basic: no captcha support but a honey pot.
*Which database is used?*
@@ -39,15 +37,15 @@ Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-ba
*Which technologies are used?*
- [Python](https://www.python.org)
- [Python 3.9](https://www.python.org)
- [Flask](http://flask.pocoo.org)
- [Peewee ORM](http://docs.peewee-orm.com)
- [Markdown](http://daringfireball.net/projects/markdown)
### Installation
Build and Dependency management relies on [Poetry](https://python-poetry.org/), but you can also use [published releases](https://github.com/kianby/stacosys/releases) or [Docker image](https://hub.docker.com/r/kianby/stacosys).
Build is based on [Poetry](https://python-poetry.org/) but you can also use [published releases on GitHub](https://github.com/kianby/stacosys/releases) or the [Docker image](https://hub.docker.com/repository/docker/kianby/stacosys).
### Improvements
Stacosys fits my needs, and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork and enhance the project if you need additional features.
Stacosys fits my needs and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork the project and enhance the project if you need more features.
+18 -11
View File
@@ -2,29 +2,36 @@
; Default configuration
[main]
lang = fr
db = sqlite://db.sqlite
db_sqlite_file = db.sqlite
db_backup_json_file = db.json
newcomment_polling = 60
[site]
name = "My blog"
proto = https
url = https://blog.mydomain.com
url = http://blog.mydomain.com
token = aabbccddeeffgghhiijjkkllmm
admin_email = admin@mydomain.com
redirect = /redirect
[http]
host = 127.0.0.1
port = 8100
[rss]
proto = https
file = comments.xml
[smtp]
host = smtp.mail.com
port = 465
[imap]
polling = 120
host = mail.gandi.net
ssl = false
port = 993
login = blog@mydomain.com
password = MYPASSWORD
[web]
username = admin
; SHA-256 hashed password (https://coding.tools/sha256)
password = 8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918
[smtp]
host = mail.gandi.net
starttls = true
ssl = false
port = 587
login = blog@mydomain.com
password = MYPASSWORD
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sqlite3
connection = sqlite3.connect("db.sqlite")
cursor = connection.cursor()
script = """
CREATE TABLE comment (
id INTEGER NOT NULL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
notified DATETIME,
created DATETIME NOT NULL,
published DATETIME,
author_name VARCHAR(255) NOT NULL,
author_site VARCHAR(255) NOT NULL,
author_gravatar varchar(255),
content TEXT NOT NULL
, ulid INTEGER);
"""
cursor.executescript(script)
connection.close()
+15 -18
View File
@@ -6,30 +6,27 @@ import sqlite3
connection = sqlite3.connect("db.sqlite")
cursor = connection.cursor()
# What script performs:
# - first, remove site table: crash here if table doesn't exist
# (compatibility test without effort)
# - remove site_id column from comment table
# What script performs:
# - first, remove site table: crash here if table doesn't exist (compatibility test without effort)
# - remove site_id colum from comment table
script = """
PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION;
DROP TABLE site;
ALTER TABLE comment RENAME TO _comment_old;
CREATE TABLE comment (
id INTEGER NOT NULL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
notified DATETIME,
created DATETIME NOT NULL,
published DATETIME,
author_name VARCHAR(255) NOT NULL,
author_site VARCHAR(255) NOT NULL,
author_gravatar varchar(255),
content TEXT NOT NULL
id INTEGER NOT NULL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
notified DATETIME,
created DATETIME NOT NULL,
published DATETIME,
author_name VARCHAR(255) NOT NULL,
author_site VARCHAR(255) NOT NULL,
author_gravatar varchar(255),
content TEXT NOT NULL
);
INSERT INTO comment (id, url, notified, created, published,
author_name, author_site, author_gravatar, content)
SELECT id, url, notified, created, published,
author_name, author_site, author_gravatar, content
INSERT INTO comment (id, url, notified, created, published, author_name, author_site, author_gravatar, content)
SELECT id, url, notified, created, published, author_name, author_site, author_gravatar, content
FROM _comment_old;
DROP TABLE _comment_old;
COMMIT;
@@ -37,4 +34,4 @@ PRAGMA foreign_keys = ON;
"""
cursor.executescript(script)
connection.close()
connection.close()
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
cd /stacosys
python3 run.py /config/config.ini
-4
View File
@@ -1,4 +0,0 @@
[flake8]
max-line-length = 88
extend-ignore = E203
spellcheck-targets=comments
Generated
+524 -582
View File
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -1,29 +1,30 @@
[tool.poetry]
name = "stacosys"
version = "3.3"
version = "2.0b4"
description = "STAtic COmmenting SYStem"
authors = ["Yax"]
readme = "README.md"
include = ["run.py"]
[tool.poetry.dependencies]
python = "~3.11"
python = "^3.9"
apscheduler = "^3.6.3"
pyrss2gen = "^1.1"
profig = "^0.5.1"
markdown = "^3.1.1"
flask_apscheduler = "^1.11.0"
tinydb = "^4.3.0"
Flask = "^1.1.2"
peewee = "^3.14.0"
requests = "^2.25.1"
coverage = "^6.5"
background = "^0.2.1"
Flask = "^2.1.1"
types-markdown = "^3.4.2.1"
pydal = "^20221110.1"
[tool.poetry.group.dev.dependencies]
pylint = "^2.15"
mypy = "^0.991"
pytest = "^7.2.0"
coveralls = "^3.3.1"
pytest-cov = "^4.0.0"
black = "^22.10.0"
[tool.poetry.dev-dependencies]
rope = "^0.16.0"
mypy = "^0.790"
flake8-black = "^0.2.1"
black = "^20.8b1"
pytest = "^6.2.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys
import os
import argparse
import logging
from stacosys.conf.config import Config, ConfigParameter
from stacosys.core import database
from stacosys.core.rss import Rss
from stacosys.core.mailer import Mailer
from stacosys.interface import app
from stacosys.interface import api
from stacosys.interface import form
from stacosys.interface import scheduler
# configure logging
def configure_logging(level):
root_logger = logging.getLogger()
root_logger.setLevel(level)
ch = logging.StreamHandler()
ch.setLevel(level)
# create formatter
formatter = logging.Formatter("[%(asctime)s] %(name)s %(levelname)s %(message)s")
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
root_logger.addHandler(ch)
def stacosys_server(config_pathname):
# configure logging
logger = logging.getLogger(__name__)
configure_logging(logging.INFO)
logging.getLogger("werkzeug").level = logging.WARNING
logging.getLogger("apscheduler.executors").level = logging.WARNING
# check config file exists
if not os.path.isfile(config_pathname):
logger.error(f"Configuration file '{config_pathname}' not found.")
sys.exit(1)
# initialize config
conf = Config.load(config_pathname)
logger.info(conf.__repr__())
# check database file exists (prevents from creating a fresh db)
db_pathname = conf.get(ConfigParameter.DB_SQLITE_FILE)
if not os.path.isfile(db_pathname):
logger.error(f"Database file '{db_pathname}' not found.")
sys.exit(1)
# initialize database
db = database.Database()
db.setup(db_pathname)
logger.info("Start Stacosys application")
# generate RSS for all sites
rss = Rss(
conf.get(ConfigParameter.LANG),
conf.get(ConfigParameter.RSS_FILE),
conf.get(ConfigParameter.RSS_PROTO),
conf.get(ConfigParameter.SITE_NAME),
conf.get(ConfigParameter.SITE_URL),
)
rss.generate()
# configure mailer
mailer = Mailer(
conf.get(ConfigParameter.IMAP_HOST),
conf.get_int(ConfigParameter.IMAP_PORT),
conf.get_bool(ConfigParameter.IMAP_SSL),
conf.get(ConfigParameter.IMAP_LOGIN),
conf.get(ConfigParameter.IMAP_PASSWORD),
conf.get(ConfigParameter.SMTP_HOST),
conf.get_int(ConfigParameter.SMTP_PORT),
conf.get_bool(ConfigParameter.SMTP_STARTTLS),
conf.get_bool(ConfigParameter.SMTP_SSL),
conf.get(ConfigParameter.SMTP_LOGIN),
conf.get(ConfigParameter.SMTP_PASSWORD),
)
# configure scheduler
scheduler.configure(
conf.get_int(ConfigParameter.IMAP_POLLING),
conf.get_int(ConfigParameter.COMMENT_POLLING),
conf.get(ConfigParameter.LANG),
conf.get(ConfigParameter.SITE_NAME),
conf.get(ConfigParameter.SITE_TOKEN),
conf.get(ConfigParameter.SITE_ADMIN_EMAIL),
mailer,
rss,
)
# inject config parameters into flask
app.config.update(SITE_TOKEN=conf.get(ConfigParameter.SITE_TOKEN))
logger.info(f"start interfaces {api} {form}")
# start Flask
app.run(
host=conf.get(ConfigParameter.HTTP_HOST),
port=conf.get(ConfigParameter.HTTP_PORT),
debug=False,
use_reloader=False,
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("config", help="config path name")
args = parser.parse_args()
stacosys_server(args.config)
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
python3 stacosys/run.py "$@"
python3 run.py "$@"
-19
View File
@@ -1,19 +0,0 @@
{
"folders":
[
{
"path": ".",
"folder_exclude_patterns": ["__pycache__",".pytest_cache", ".mypy_cache"],
"file_exclude_patterns": [".coverage"]
}
],
"settings": {
"LSP": {
"LSP-pyright": {
"settings": {
"python.analysis.diagnosticMode": "workspace"
}
}
}
}
}
-536
View File
@@ -1,536 +0,0 @@
{
"auto_complete":
{
"selected_items":
[
[
"init",
"init_config"
],
[
"author",
"author_name="
],
[
"autho",
"author_name"
],
[
"au",
"author_gravatar="
],
[
"auth",
"author_site="
],
[
"db",
"db_uri"
],
[
"db_",
"db_uri"
],
[
"r",
"rss"
],
[
"c",
"check"
],
[
"con",
"configure_destination"
],
[
"EXP",
"EXPECTED_HTTP_PORT"
],
[
"l",
"login"
],
[
"assertI",
"assertIsNot"
],
[
"Confi",
"ConfigParameter"
],
[
"S",
"SITE_URL"
],
[
"Config",
"ConfigParameter"
],
[
"s",
"SITE_REDIRECT"
],
[
"get",
"get_int"
]
]
},
"buffers":
[
{
"file": "stacosys/service/rssfeed.py",
"settings":
{
"buffer_size": 1754,
"line_ending": "Unix"
}
}
],
"build_system": "",
"build_system_choices":
[
],
"build_varint": "",
"command_palette":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
[
"dia",
"LSP: Toggle Diagnostics Panel"
],
[
"black",
"python-black: Create Black Configuration File"
],
[
"insta",
"Package Control: Install Package"
],
[
"break",
"Python Breakpoints: Toggle"
],
[
"togg",
"Python Breakpoints: Toggle"
],
[
"diag",
"LSP: Toggle Diagnostics Panel"
],
[
"lsp",
"LSP: Toggle Diagnostics Panel"
],
[
"comment",
"Toggle Comment"
],
[
"install",
"Package Control: Install Package"
],
[
"rename",
"LSP: Rename"
],
[
"brea",
"Python Breakpoints: Clear All"
],
[
"comm",
"Toggle Comment"
],
[
"move",
"File: Move…"
],
[
"remove",
"Package Control: Remove Package"
],
[
"make",
"Build With: Make"
],
[
"python",
"Python Breakpoints: Toggle"
],
[
"docstr",
"AutoDocstring: Current"
],
[
"Package Control: ",
"Package Control: Install Package"
],
[
"distr",
"View: Toggle Distraction Free"
],
[
"upg",
"Package Control: Upgrade Package"
],
[
"form",
"LSP: Format Document"
],
[
"format",
"LSP: Format Document"
],
[
"lspc",
"LSP: Clear Diagnostics"
],
[
"ren",
"LSP: Rename Symbol"
],
[
"instal",
"Package Control: Install Package"
],
[
"package re",
"Package Control: Remove Package"
],
[
"paka",
"Package Control: Remove Package"
],
[
"inst",
"Package Control: Install Package"
],
[
"pac",
"Package Control: Remove Package"
]
],
"width": 0.0
},
"console":
{
"height": 164.0,
"history":
[
]
},
"distraction_free":
{
"menu_visible": true,
"show_minimap": false,
"show_open_files": false,
"show_tabs": false,
"side_bar_visible": false,
"status_bar_visible": false
},
"expanded_folders":
[
"/home/yannic/work/stacosys",
"/home/yannic/work/stacosys/dbmigration",
"/home/yannic/work/stacosys/docker",
"/home/yannic/work/stacosys/stacosys",
"/home/yannic/work/stacosys/stacosys/db",
"/home/yannic/work/stacosys/stacosys/interface",
"/home/yannic/work/stacosys/stacosys/interface/web",
"/home/yannic/work/stacosys/stacosys/model",
"/home/yannic/work/stacosys/stacosys/service",
"/home/yannic/work/stacosys/tests"
],
"file_history":
[
"/home/yannic/work/stacosys/stacosys/db/__init__.py",
"/home/yannic/work/stacosys/tests/test_api.py",
"/home/yannic/work/stacosys/tests/test_db.py",
"/home/yannic/work/stacosys/stacosys/service/configuration.py",
"/home/yannic/work/stacosys/Makefile",
"/home/yannic/work/stacosys/stacosys/db/dao.py",
"/home/yannic/work/stacosys/pyproject.toml",
"/home/yannic/work/stacosys/stacosys/run.py",
"/home/yannic/work/stacosys/config-dev.ini",
"/home/yannic/work/stacosys/dbmigration/migrate_from_3.3_to_4.0.py",
"/home/yannic/work/stacosys/stacosys/service/rssfeed.py",
"/home/yannic/work/stacosys/stacosys/model/comment.py",
"/home/yannic/work/stacosys/tests/test_form.py",
"/home/yannic/work/stacosys/config.ini",
"/home/yannic/work/stacosys/tests/test_config.py",
"/home/yannic/work/stacosys/dbmigration/migrate_from_1.1_to_2.0.py",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/pydal/objects.py",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/markdown/extensions/def_list.py",
"/home/yannic/work/stacosys/stacosys/db/database.py",
"/home/yannic/work/stacosys/tests/test_rssfeed.py",
"/home/yannic/work/stacosys/stacosys/service/mail.py",
"/home/yannic/work/stacosys/stacosys/interface/form.py",
"/home/yannic/work/stacosys/stacosys/interface/__init__.py",
"/home/yannic/work/stacosys/stacosys/interface/api.py",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/peewee.py",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/background.py",
"/home/yannic/work/stacosys/tests/test_mail.py",
"/home/yannic/.cache/sublime-text-3/Package Storage/LSP-pyright/18.7.0/language-server/node_modules/pyright/dist/typeshed-fallback/stdlib/builtins.pyi",
"/home/yannic/work/stacosys/.venv/lib64/python3.11/site-packages/mypy/typeshed/stdlib/socket.pyi",
"/usr/lib64/python3.11/smtplib.py",
"/home/yannic/work/stacosys/stacosys/interface/web/admin.py",
"/home/yannic/work/stacosys/comments.xml",
"/home/yannic/work/stacosys/stacosys/service/__init__.py",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/PyRSS2Gen.py",
"/home/yannic/work/blog/README.md",
"/home/yannic/work/blog/Dockerfile",
"/usr/lib64/python3.11/logging/__init__.py",
"/home/yannic/work/stacosys/stacosys/core/__init__.py",
"/home/yannic/work/stacosys/stacosys/core/configuration.py",
"/home/yannic/work/stacosys/.pylintrc",
"/home/yannic/work/stacosys/stacosys/core/mailer.py",
"/home/yannic/work/stacosys/stacosys/conf/config.py",
"/home/yannic/work/stacosys/stacosys/core/rss.py",
"/home/yannic/work/stacosys/run.sh",
"/home/yannic/work/stacosys/stacosys.sublime-project",
"/home/yannic/work/stacosys/pylintrc",
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/flask/app.py",
"/home/yannic/.cache/sublime-text-3/Package Storage/LSP-pyright/18.7.0/language-server/node_modules/pyright/dist/typeshed-fallback/stdlib/sys.pyi",
"/home/yannic/work/stacosys/flake8.ini",
"/home/yannic/work/stacosys/stacosys/__init__.py"
],
"find":
{
"height": 28.0
},
"find_in_files":
{
"height": 104.0,
"where_history":
[
""
]
},
"find_state":
{
"case_sensitive": false,
"find_history":
[
"bla",
"find_not_published_comments",
"find_comment_by_id",
"asdict",
"def delete",
"db_dal",
"tox",
"apscheduler",
"_lang",
"config",
"SITE_TOKE",
"app.conf",
"disable",
"background"
],
"highlight": true,
"in_selection": false,
"preserve_case": false,
"regex": false,
"replace_history":
[
],
"reverse": false,
"scrollbar_highlights": true,
"show_context": true,
"use_buffer2": true,
"use_gitignore": true,
"whole_word": false,
"wrap": true
},
"groups":
[
{
"sheets":
[
{
"buffer": 0,
"file": "stacosys/service/rssfeed.py",
"selected": true,
"semi_transient": true,
"settings":
{
"buffer_size": 1754,
"regions":
{
},
"selection":
[
[
0,
0
]
],
"settings":
{
"auto_complete_triggers":
[
{
"characters": "<",
"selector": "text.html, text.xml"
},
{
"rhs_empty": true,
"selector": "punctuation.accessor"
},
{
"characters": ".[",
"selector": "meta.tag, source - comment - string.quoted.double.block - string.quoted.single.block - string.unquoted.heredoc",
"server": "LSP-pyright"
}
],
"lsp_active": true,
"lsp_hover_provider_count": 1,
"lsp_uri": "file:///home/yannic/work/stacosys/stacosys/service/rssfeed.py",
"show_definitions": false,
"syntax": "Packages/Python/Python.sublime-syntax",
"tab_size": 4,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 0,
"stack_multiselect": false,
"type": "text"
}
]
}
],
"incremental_find":
{
"height": 28.0
},
"input":
{
"height": 40.0
},
"layout":
{
"cells":
[
[
0,
0,
1,
1
]
],
"cols":
[
0.0,
1.0
],
"rows":
[
0.0,
1.0
]
},
"menu_visible": true,
"output.LSP Log Panel":
{
"height": 0.0
},
"output.diagnostics":
{
"height": 261.0
},
"output.exec":
{
"height": 132.0
},
"output.find_results":
{
"height": 0.0
},
"output.language servers":
{
"height": 132.0
},
"output.mdpopups":
{
"height": 0.0
},
"output.references":
{
"height": 208.0
},
"pinned_build_system": "",
"project": "stacosys.sublime-project",
"replace":
{
"height": 52.0
},
"save_all_on_build": true,
"select_file":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
[
"def del",
".venv/lib/python3.11/site-packages/markdown/extensions/def_list.py"
],
[
"socket.p",
".venv/lib64/python3.11/site-packages/mypy/typeshed/stdlib/socket.pyi"
],
[
"mail",
"stacosys/service/mail.py"
],
[
"conf",
"stacosys/service/configuration.py"
]
],
"width": 0.0
},
"select_project":
{
"height": 500.0,
"last_filter": "",
"selected_items":
[
[
"",
"~/work/blog/blog.sublime-project"
]
],
"width": 380.0
},
"select_symbol":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"selected_group": 0,
"settings":
{
},
"show_minimap": true,
"show_open_files": false,
"show_tabs": true,
"side_bar_visible": true,
"side_bar_width": 299.0,
"status_bar_visible": true,
"template_settings":
{
}
}
+1
View File
@@ -0,0 +1 @@
__version__ = "2.0"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from enum import Enum
import profig
class ConfigParameter(Enum):
DB_SQLITE_FILE = "main.db_sqlite_file"
DB_BACKUP_JSON_FILE = "main.db_backup_json_file"
LANG = "main.lang"
COMMENT_POLLING = "main.newcomment_polling"
HTTP_HOST = "http.host"
HTTP_PORT = "http.port"
RSS_PROTO = "rss.proto"
RSS_FILE = "rss.file"
IMAP_POLLING = "imap.polling"
IMAP_SSL = "imap.ssl"
IMAP_HOST = "imap.host"
IMAP_PORT = "imap.port"
IMAP_LOGIN = "imap.login"
IMAP_PASSWORD = "imap.password"
SMTP_STARTTLS = "smtp.starttls"
SMTP_SSL = "smtp.ssl"
SMTP_HOST = "smtp.host"
SMTP_PORT = "smtp.port"
SMTP_LOGIN = "smtp.login"
SMTP_PASSWORD = "smtp.password"
SITE_NAME = "site.name"
SITE_URL = "site.url"
SITE_TOKEN = "site.token"
SITE_ADMIN_EMAIL = "site.admin_email"
class Config:
def __init__(self):
self._params = dict()
@classmethod
def load(cls, config_pathname):
cfg = profig.Config(config_pathname)
cfg.sync()
config = cls()
config._params.update(cfg)
return config
def exists(self, key: ConfigParameter):
return key.value in self._params
def get(self, key: ConfigParameter):
return self._params[key.value] if key.value in self._params else None
def put(self, key: ConfigParameter, value):
self._params[key.value] = value
def get_int(self, key: ConfigParameter):
return int(self._params[key.value])
def get_bool(self, key: ConfigParameter):
return self._params[key.value].lower() in ("yes", "true")
def __repr__(self):
return self._params.__repr__()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import re
from stacosys.core.templater import Templater, Template
from stacosys.model.comment import Comment
from stacosys.model.email import Email
from stacosys.core.rss import Rss
from stacosys.core.mailer import Mailer
logger = logging.getLogger(__name__)
current_path = os.path.dirname(__file__)
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
templater = Templater(template_path)
def fetch_mail_answers(lang, mailer: Mailer, rss: Rss, site_token):
for msg in mailer.fetch():
if re.search(r".*STACOSYS.*\[(\d+)\:(\w+)\]", msg.subject, re.DOTALL):
if _reply_comment_email(lang, mailer, rss, msg, site_token):
mailer.delete(msg.id)
def _reply_comment_email(lang, mailer: Mailer, rss: Rss, email: Email, site_token):
m = re.search(r"\[(\d+)\:(\w+)\]", email.subject)
if not m:
logger.warn("ignore corrupted email. No token %s" % email.subject)
return
comment_id = int(m.group(1))
token = m.group(2)
if token != site_token:
logger.warn("ignore corrupted email. Unknown token %d" % comment_id)
return
# retrieve site and comment rows
comment = Comment.get_by_id(comment_id)
if not comment:
logger.warn("unknown comment %d" % comment_id)
return True
if comment.published:
logger.warn("ignore already published email. token %d" % comment_id)
return
if not email.plain_text_content:
logger.warn("ignore empty email")
return
# safe logic: no answer or unknown answer is a go for publishing
if email.plain_text_content[:2].upper() in ("NO"):
logger.info("discard comment: %d" % comment_id)
comment.delete_instance()
new_email_body = templater.get_template(lang, Template.DROP_COMMENT).render(
original=email.plain_text_content
)
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
logger.warn("minor failure. cannot send rejection mail " + email.subject)
else:
# save publishing datetime
comment.publish()
logger.info("commit comment: %d" % comment_id)
# rebuild RSS
rss.generate()
# send approval confirmation email to admin
new_email_body = templater.get_template(lang, Template.APPROVE_COMMENT).render(
original=email.plain_text_content
)
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
logger.warn("minor failure. cannot send approval email " + email.subject)
return True
def submit_new_comment(lang, site_name, site_token, site_admin_email, mailer):
for comment in Comment.select().where(Comment.notified.is_null()):
comment_list = (
"author: %s" % comment.author_name,
"site: %s" % comment.author_site,
"date: %s" % comment.created,
"url: %s" % comment.url,
"",
"%s" % comment.content,
"",
)
comment_text = "\n".join(comment_list)
email_body = templater.get_template(lang, Template.NEW_COMMENT).render(
url=comment.url, comment=comment_text
)
# send email
subject = "STACOSYS %s: [%d:%s]" % (site_name, comment.id, site_token)
if mailer.send(site_admin_email, subject, email_body):
logger.debug("new comment processed ")
# notify site admin and save notification datetime
comment.notify_site_admin()
else:
logger.warn("rescheduled. send mail failure " + subject)
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import json
from peewee import Model
from playhouse.db_url import SqliteDatabase
from playhouse.shortcuts import model_to_dict
from tinydb import TinyDB
db = SqliteDatabase(None)
class BaseModel(Model):
class Meta:
database = db
class Database:
def get_db(self):
return db
def setup(self, db_url):
db.init(db_url)
db.connect()
from stacosys.model.comment import Comment
db.create_tables([Comment], safe=True)
# if config.exists(config.DB_BACKUP_JSON_FILE):
# _backup_db(config.DB_BACKUP_JSON_FILE, Comment)
def _tojson_model(comment):
dcomment = model_to_dict(comment)
# del dcomment["site"]
tcomment = json.dumps(dcomment, indent=4, sort_keys=True, default=str)
return json.loads(tcomment)
def _backup_db(db_file, Comment):
db = TinyDB(db_file, sort_keys=True, indent=4, separators=(",", ": "))
db.drop_tables()
table = db.table("comments")
for comment in Comment.select():
cc = _tojson_model(comment)
table.insert(cc)
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import base64
import datetime
import email
import imaplib
import logging
import re
from email.message import Message
from stacosys.model.email import Attachment, Email, Part
filename_re = re.compile('filename="(.+)"|filename=([^;\n\r"\']+)', re.I | re.S)
class Mailbox(object):
def __init__(self, host, port, ssl, login, password):
self.logger = logging.getLogger(__name__)
self.host = host
self.port = port
self.ssl = ssl
self.login = login
self.password = password
def __enter__(self):
if self.ssl:
self.imap = imaplib.IMAP4_SSL(self.host, self.port)
else:
self.imap = imaplib.IMAP4(self.host, self.port)
self.imap.login(self.login, self.password)
return self
def __exit__(self, type, value, traceback):
self.imap.close()
self.imap.logout()
def get_count(self):
self.imap.select("Inbox")
_, data = self.imap.search(None, "ALL")
return sum(1 for num in data[0].split())
def fetch_raw_message(self, num):
self.imap.select("Inbox")
_, data = self.imap.fetch(str(num), "(RFC822)")
email_msg = email.message_from_bytes(data[0][1])
return email_msg
def fetch_message(self, num):
raw_msg = self.fetch_raw_message(num)
parts = []
attachments = []
plain_text_content = "no plain-text part"
for part in raw_msg.walk():
if part.is_multipart():
continue
content_disposition = part.get("Content-Disposition", None)
if content_disposition:
# we have attachment
r = filename_re.findall(content_disposition)
if r:
filename = sorted(r[0])[1]
else:
filename = "undefined"
content = base64.b64encode(part.get_payload(decode=True))
content = content.decode()
attachments.append(
Attachment(
filename=email_nonascii_to_uft8(filename),
content=content,
content_type=part.get_content_type(),
)
)
else:
try:
content = to_plain_text_content(part)
except Exception:
logging.exception("cannot extract content from mail part")
parts.append(
Part(content=content, content_type=part.get_content_type())
)
if part.get_content_type() == "text/plain":
plain_text_content = content
return Email(
id=num,
encoding="UTF-8",
date=parse_date(raw_msg["Date"]).strftime("%Y-%m-%d %H:%M:%S"),
from_addr=raw_msg["From"],
to_addr=raw_msg["To"],
subject=email_nonascii_to_uft8(raw_msg["Subject"]),
parts=parts,
attachments=attachments,
plain_text_content=plain_text_content,
)
def delete_message(self, num):
self.imap.select("Inbox")
self.imap.store(str(num), "+FLAGS", r"\Deleted")
self.imap.expunge()
def delete_all(self):
self.imap.select("Inbox")
_, data = self.imap.search(None, "ALL")
for num in data[0].split():
self.imap.store(num, "+FLAGS", r"\Deleted")
self.imap.expunge()
def print_msgs(self):
self.imap.select("Inbox")
_, data = self.imap.search(None, "ALL")
for num in reversed(data[0].split()):
status, data = self.imap.fetch(num, "(RFC822)")
self.logger.debug("Message %s\n%s\n" % (num, data[0][1]))
def parse_date(v):
if v is None:
return datetime.datetime.now()
tt = email.utils.parsedate_tz(v)
if tt is None:
return datetime.datetime.now()
timestamp = email.utils.mktime_tz(tt)
date = datetime.datetime.fromtimestamp(timestamp)
return date
def to_utf8(string, charset):
return string.decode(charset).encode("UTF-8").decode("UTF-8")
def email_nonascii_to_uft8(string):
# RFC 1342 is a recommendation that provides a way to represent non ASCII
# characters inside e-mail in a way that wont confuse e-mail servers
subject = ""
for v, charset in email.header.decode_header(string):
if charset is None:
if type(v) is bytes:
v = v.decode()
subject = subject + v
else:
subject = subject + to_utf8(v, charset)
return subject
def to_plain_text_content(part: Message) -> str:
content = part.get_payload(decode=True)
charset = part.get_param("charset", None)
if charset:
content = to_utf8(content, charset)
elif type(content) == bytes:
content = content.decode("utf8")
# RFC 3676: remove automatic word-wrapping
return content.replace(" \r\n", " ")
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import smtplib
from email.mime.text import MIMEText
from stacosys.core import imap
logger = logging.getLogger(__name__)
class Mailer:
def __init__(
self,
imap_host,
imap_port,
imap_ssl,
imap_login,
imap_password,
smtp_host,
smtp_port,
smtp_starttls,
smtp_ssl,
smtp_login,
smtp_password,
):
self._imap_host = imap_host
self._imap_port = imap_port
self._imap_ssl = imap_ssl
self._imap_login = imap_login
self._imap_password = imap_password
self._smtp_host = smtp_host
self._smtp_port = smtp_port
self._smtp_starttls = smtp_starttls
self._smtp_ssl = smtp_ssl
self._smtp_login = smtp_login
self._smtp_password = smtp_password
def _open_mailbox(self):
return imap.Mailbox(
self._imap_host,
self._imap_port,
self._imap_ssl,
self._imap_login,
self._imap_password,
)
def fetch(self):
msgs = []
try:
with self._open_mailbox() as mbox:
count = mbox.get_count()
for num in range(count):
msgs.append(mbox.fetch_message(num + 1))
except Exception:
logger.exception("fetch mail exception")
return msgs
def send(self, to_email, subject, message):
# Create the container (outer) email message.
msg = MIMEText(message)
msg["Subject"] = subject
msg["To"] = to_email
msg["From"] = self._smtp_login
success = True
try:
if self._smtp_ssl:
s = smtplib.SMTP_SSL(self._smtp_host, self._smtp_port)
else:
s = smtplib.SMTP(self._smtp_host, self._smtp_port)
if self._smtp_starttls:
s.starttls()
s.login(self._smtp_login, self._smtp_password)
s.send_message(msg)
s.quit()
except Exception:
logger.exception("send mail exception")
success = False
return success
def delete(self, id):
try:
with self._open_mailbox() as mbox:
mbox.delete_message(id)
except Exception:
logger.exception("delete mail exception")
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
from datetime import datetime
import markdown
import PyRSS2Gen
from stacosys.core.templater import Templater, Template
from stacosys.model.comment import Comment
class Rss:
def __init__(
self,
lang,
rss_file,
rss_proto,
site_name,
site_url,
):
self._lang = lang
self._rss_file = rss_file
self._rss_proto = rss_proto
self._site_name = site_name
self._site_url = site_url
current_path = os.path.dirname(__file__)
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
self._templater = Templater(template_path)
def generate(self):
rss_title = self._templater.get_template(
self._lang, Template.RSS_TITLE_MESSAGE
).render(site=self._site_name)
md = markdown.Markdown()
items = []
for row in (
Comment.select()
.where(Comment.published)
.order_by(-Comment.published)
.limit(10)
):
item_link = "%s://%s%s" % (self._rss_proto, self._site_url, row.url)
items.append(
PyRSS2Gen.RSSItem(
title="%s - %s://%s%s"
% (self._rss_proto, row.author_name, self._site_url, row.url),
link=item_link,
description=md.convert(row.content),
guid=PyRSS2Gen.Guid("%s/%d" % (item_link, row.id)),
pubDate=row.published,
)
)
rss = PyRSS2Gen.RSS2(
title=rss_title,
link="%s://%s" % (self._rss_proto, self._site_url),
description='Commentaires du site "%s"' % self._site_name,
lastBuildDate=datetime.now(),
items=items,
)
rss.write_xml(open(self._rss_file, "w"), encoding="utf-8")
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from enum import Enum
from jinja2 import Environment, FileSystemLoader
class Template(Enum):
DROP_COMMENT = "drop_comment"
APPROVE_COMMENT = "approve_comment"
NEW_COMMENT = "new_comment"
NOTIFY_MESSAGE = "notify_message"
RSS_TITLE_MESSAGE = "rss_title_message"
class Templater:
def __init__(self, template_path):
self._env = Environment(loader=FileSystemLoader(template_path))
def get_template(self, lang, template: Template):
return self._env.get_template(lang + "/" + template.value + ".tpl")
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pydal import DAL, Field
class Database:
db_dal = DAL()
def configure(self, db_uri):
self.db_dal = DAL(db_uri, migrate=db_uri.startswith("sqlite:memory"))
self.db_dal.define_table(
"comment",
Field("url"),
Field("created", type="datetime"),
Field("notified", type="datetime"),
Field("published", type="datetime"),
Field("author_name"),
Field("author_site"),
Field("author_gravatar"),
Field("content", type="text"),
)
def get(self):
return self.db_dal
database = Database()
db = database.get
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
# pylint: disable=singleton-comparison
from datetime import datetime
from stacosys.db import db
from stacosys.model.comment import Comment
def find_comment_by_id(comment_id):
return db().comment(comment_id)
def notify_comment(comment: Comment):
db()(db().comment.id == comment.id).update(notified=datetime.now())
db().commit()
def publish_comment(comment: Comment):
db()(db().comment.id == comment.id).update(published=datetime.now())
db().commit()
def delete_comment(comment: Comment):
db()(db().comment.id == comment.id).delete()
db().commit()
def find_not_notified_comments():
return db()(db().comment.notified == None).select()
def find_not_published_comments():
return db()(db().comment.published == None).select()
def find_published_comments_by_url(url):
return db()((db().comment.url == url) & (db().comment.published != None)).select(
orderby=db().comment.published
)
def count_published_comments(url):
return (
db()((db().comment.url == url) & (db().comment.published != None)).count()
if url
else db()(db().comment.published != None).count()
)
def find_recent_published_comments():
return db()(db().comment.published != None).select(
orderby=~db().comment.published, limitby=(0, 10)
)
def create_comment(url, author_name, author_site, author_gravatar, message):
row = db().comment.insert(
url=url,
author_name=author_name,
author_site=author_site,
author_gravatar=author_gravatar,
content=message,
created=datetime.now(),
notified=None,
published=None,
)
db().commit()
return Comment(
id=row.id,
url=row.url,
author_name=row.author_name,
author_site=row.author_site,
author_gravatar=row.author_gravatar,
content=row.content,
created=row.created,
notified=row.notified,
published=row.published,
)
-37
View File
@@ -1,43 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import background
from flask import Flask
from stacosys.db import dao
from stacosys.service import config, mailer
from stacosys.service.configuration import ConfigParameter
app = Flask(__name__)
# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
logger = logging.getLogger(__name__)
@background.task
def submit_new_comment(comment):
site_url = config.get(ConfigParameter.SITE_URL)
comment_list = (
f"Web admin interface: {site_url}/web/admin",
"",
f"author: {comment.author_name}",
f"site: {comment.author_site}",
f"date: {comment.created}",
f"url: {comment.url}",
"",
comment.content,
"",
)
email_body = "\n".join(comment_list)
# send email to notify admin
site_name = config.get(ConfigParameter.SITE_NAME)
subject = f"STACOSYS {site_name}"
if mailer.send(subject, email_body):
logger.debug("new comment processed")
# save notification datetime
dao.notify_comment(comment)
+58 -26
View File
@@ -2,44 +2,76 @@
# -*- coding: utf-8 -*-
import logging
from flask import abort, jsonify, request
from flask import jsonify, request
from stacosys.db import dao
from stacosys.interface import app, submit_new_comment
from stacosys.interface import app
from stacosys.model.comment import Comment
logger = logging.getLogger(__name__)
@app.route("/api/ping", methods=["GET"])
@app.route("/ping", methods=["GET"])
def ping():
return "OK"
@app.route("/api/comments", methods=["GET"])
@app.route("/comments", methods=["GET"])
def query_comments():
comments = []
url = request.args.get("url", "")
try:
token = request.args.get("token", "")
if token != app.config.get("SITE_TOKEN"):
abort(401)
logger.info("retrieve comments for url %s", url)
for comment in dao.find_published_comments_by_url(url):
comment_dto = {
"author": comment.author_name,
"content": comment.content,
"avatar": comment.author_gravatar,
"date": comment.published.strftime("%Y-%m-%d %H:%M:%S"),
}
if comment.author_site:
comment_dto["site"] = comment.author_site
logger.debug(comment_dto)
comments.append(comment_dto)
return jsonify({"data": comments})
url = request.args.get("url", "")
logger.info("retrieve comments for url %s" % (url))
for comment in (
Comment.select(Comment)
.where((Comment.url == url) & (Comment.published.is_null(False)))
.order_by(+Comment.published)
):
d = {
"author": comment.author_name,
"content": comment.content,
"avatar": comment.author_gravatar,
"date": comment.published.strftime("%Y-%m-%d %H:%M:%S"),
}
if comment.author_site:
d["site"] = comment.author_site
logger.debug(d)
comments.append(d)
r = jsonify({"data": comments})
r.status_code = 200
except Exception:
logger.warn("bad request")
r = jsonify({"data": []})
r.status_code = 400
return r
@app.route("/api/comments/count", methods=["GET"])
@app.route("/comments/count", methods=["GET"])
def get_comments_count():
# send notification for pending e-mails asynchronously
for comment in dao.find_not_notified_comments():
submit_new_comment(comment)
url = request.args.get("url", "")
return jsonify({"count": dao.count_published_comments(url)})
try:
token = request.args.get("token", "")
if token != app.config.get("SITE_TOKEN"):
abort(401)
url = request.args.get("url", "")
if url:
count = (
Comment.select(Comment)
.where((Comment.url == url) & (Comment.published.is_null(False)))
.count()
)
else:
count = (
Comment.select(Comment).where(Comment.published.is_null(False)).count()
)
r = jsonify({"count": count})
r.status_code = 200
except Exception:
r = jsonify({"count": 0})
r.status_code = 200
return r
+58 -39
View File
@@ -1,56 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import logging
from datetime import datetime
from flask import abort, redirect, request
from stacosys.db import dao
from stacosys.interface import app, submit_new_comment
from stacosys.service import config
from stacosys.service.configuration import ConfigParameter
from stacosys.interface import app
from stacosys.model.comment import Comment
logger = logging.getLogger(__name__)
@app.route("/newcomment", methods=["POST"])
def new_form_comment():
data = request.form
logger.info("form data %s", str(data))
# honeypot for spammers
captcha = data.get("remarque", "")
if captcha:
logger.warning("discard spam: data %s", data)
try:
data = request.form
logger.info("form data " + str(data))
# validate token: retrieve site entity
token = data.get("token", "")
if token != app.config.get("SITE_TOKEN"):
abort(401)
# honeypot for spammers
captcha = data.get("remarque", "")
if captcha:
logger.warn("discard spam: data %s" % data)
abort(400)
url = data.get("url", "")
author_name = data.get("author", "").strip()
author_gravatar = data.get("email", "").strip()
author_site = data.get("site", "").lower().strip()
if author_site and author_site[:4] != "http":
author_site = "http://" + author_site
message = data.get("message", "")
# anti-spam again
if not url or not author_name or not message:
logger.warn("empty field: data %s" % data)
abort(400)
check_form_data(data)
# add a row to Comment table
created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
comment = Comment(
url=url,
author_name=author_name,
author_site=author_site,
author_gravatar=author_gravatar,
content=message,
created=created,
notified=None,
published=None,
)
comment.save()
except Exception:
logger.exception("new comment failure")
abort(400)
url = data.get("url", "")
author_name = data.get("author", "").strip()
author_gravatar = data.get("email", "").strip()
author_site = data.get("site", "").lower().strip()
if author_site and author_site[:4] != "http":
author_site = "http://" + author_site
message = data.get("message", "")
# anti-spam again
if not url or not author_name or not message:
logger.warning("empty field: data %s", data)
abort(400)
if not check_form_data(data.to_dict()):
logger.warning("additional field: data %s", data)
abort(400)
# add a row to Comment table
comment = dao.create_comment(
url, author_name, author_site, author_gravatar, message
)
# send notification e-mail asynchronously
submit_new_comment(comment)
return redirect(config.get(ConfigParameter.SITE_REDIRECT), code=302)
return redirect("/redirect/", code=302)
def check_form_data(posted_comment):
def check_form_data(data):
fields = ["url", "message", "site", "remarque", "author", "token", "email"]
filtered = dict(filter(lambda x: x[0] not in fields, posted_comment.items()))
return not filtered
d = data.to_dict()
for field in fields:
if field in d:
del d[field]
if d:
logger.warn("additional field: data %s" % data)
abort(400)
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_apscheduler import APScheduler
from stacosys.interface import app
class JobConfig(object):
JOBS: list = []
SCHEDULER_EXECUTORS = {"default": {"type": "threadpool", "max_workers": 4}}
def __init__(
self,
imap_polling_seconds,
new_comment_polling_seconds,
lang,
site_name,
site_token,
site_admin_email,
mailer,
rss,
):
self.JOBS = [
{
"id": "fetch_mail",
"func": "stacosys.core.cron:fetch_mail_answers",
"args": [lang, mailer, rss, site_token],
"trigger": "interval",
"seconds": imap_polling_seconds,
},
{
"id": "submit_new_comment",
"func": "stacosys.core.cron:submit_new_comment",
"args": [lang, site_name, site_token, site_admin_email, mailer],
"trigger": "interval",
"seconds": new_comment_polling_seconds,
},
]
def configure(
imap_polling,
comment_polling,
lang,
site_name,
site_token,
site_admin_email,
mailer,
rss,
):
app.config.from_object(
JobConfig(
imap_polling,
comment_polling,
lang,
site_name,
site_token,
site_admin_email,
mailer,
rss,
)
)
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
@@ -1,64 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stacosys</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>
<body>
<header>
<h2>Modération des commentaires</h2>
<nav>
<a href="/web/logout">Déconnecter</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<blockquote>
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</blockquote>
{% endif %}
{% endwith %}
<table>
<thead>
<tr>
<th>Date</th>
<th>Auteur</th>
<th>Commentaire</th>
<th>Article</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
<tr>
<td>{{ comment.created }}</td>
<td>{{ comment.author_name }}</td>
<td>{{ comment.content }}</td>
<td><a href="{{ baseurl + comment.url }}">{{ comment.url }}</a></td>
<td>
<form action="/web/admin" method="post">
<input type="hidden" name="comment" value="{{comment.id}}">
<input type="hidden" name="action" value="APPROVE">
<button type="submit">Accepter</button>
</form>
<form action="/web/admin" method="post">
<input type="hidden" name="comment" value="{{comment.id}}">
<input type="hidden" name="action" value="REJECT">
<button type="submit">Rejeter</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
<footer>
<p>Cette page a été conçue par Yax avec <a href="https://simplecss.org">Simple.css</a>.</p>
</footer>
</body>
</html>
@@ -1,42 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stacosys</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<style>
form {
width: 350px;
margin: 0 auto;
text-align: center;
}
</style>
</head>
<body>
<header>
<h2>Modération des commentaires</h2>
</header>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<blockquote>
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</blockquote>
{% endif %}
{% endwith %}
<form action="/web/login" method="POST">
<p><label>Utilisateur </label></p>
<p><input type="text" name="username" /></p>
<p><label>Mot de passe </label></p>
<p><input type="password" name="password" /></p>
<input type="submit" value="Connecter" />
</form>
</main>
<footer>
<p>Cette page a été conçue avec <a href="https://simplecss.org">Simple.css</a>.</p>
</footer>
</body>
</html>
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib
import logging
from flask import flash, redirect, render_template, request, session
from stacosys.db import dao
from stacosys.interface import app
from stacosys.service import config, rss
from stacosys.service.configuration import ConfigParameter
logger = logging.getLogger(__name__)
app.add_url_rule("/web", endpoint="index")
app.add_url_rule("/web/", endpoint="index")
@app.endpoint("index")
def index():
return redirect("/web/admin")
def is_login_ok(username, password):
hashed = hashlib.sha256(password.encode()).hexdigest().upper()
return (
config.get(ConfigParameter.WEB_USERNAME) == username
and config.get(ConfigParameter.WEB_PASSWORD) == hashed
)
@app.route("/web/login", methods=["POST", "GET"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if is_login_ok(username, password):
session["user"] = username
return redirect("/web/admin")
# TODO localization
flash("Identifiant ou mot de passe incorrect")
return redirect("/web/login")
# GET
return render_template("login_" + config.get(ConfigParameter.LANG) + ".html")
@app.route("/web/logout", methods=["GET"])
def logout():
session.pop("user")
return redirect("/web/admin")
@app.route("/web/admin", methods=["GET"])
def admin_homepage():
if not (
"user" in session
and session["user"] == config.get(ConfigParameter.WEB_USERNAME)
):
# TODO localization
flash("Vous avez été déconnecté.")
return redirect("/web/login")
comments = dao.find_not_published_comments()
return render_template(
"admin_" + config.get(ConfigParameter.LANG) + ".html",
comments=comments,
baseurl=config.get(ConfigParameter.SITE_URL),
)
@app.route("/web/admin", methods=["POST"])
def admin_action():
comment = dao.find_comment_by_id(request.form.get("comment"))
if comment is None:
# TODO localization
flash("Commentaire introuvable")
elif request.form.get("action") == "APPROVE":
dao.publish_comment(comment)
rss.generate()
# TODO localization
flash("Commentaire publié")
else:
dao.delete_comment(comment)
# TODO localization
flash("Commentaire supprimé")
return redirect("/web/admin")
+21 -13
View File
@@ -1,19 +1,27 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from dataclasses import dataclass
from peewee import CharField
from peewee import TextField
from peewee import DateTimeField
from datetime import datetime
from typing import Optional
from stacosys.core.database import BaseModel
@dataclass
class Comment:
id: int = 0
url: str = ""
created: Optional[datetime] = None
notified: Optional[datetime] = None
published: Optional[datetime] = None
author_name: str = ""
author_site: str = ""
author_gravatar: str = ""
content: str = ""
class Comment(BaseModel):
url = CharField()
created = DateTimeField()
notified = DateTimeField(null=True, default=None)
published = DateTimeField(null=True, default=None)
author_name = CharField()
author_site = CharField(default="")
author_gravatar = CharField(default="")
content = TextField()
def notify_site_admin(self):
self.notified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.save()
def publish(self):
self.published = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.save()
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import argparse
import logging
import os
import sys
from stacosys.db import database
from stacosys.interface import api, app, form
from stacosys.interface.web import admin
from stacosys.service import config, mailer, rss
from stacosys.service.configuration import ConfigParameter
# configure logging
def configure_logging(level):
root_logger = logging.getLogger()
root_logger.setLevel(level)
handler = logging.StreamHandler()
handler.setLevel(level)
formatter = logging.Formatter("[%(asctime)s] %(name)s %(levelname)s %(message)s")
handler.setFormatter(formatter)
root_logger.addHandler(handler)
def stacosys_server(config_pathname):
# configure logging
logger = logging.getLogger(__name__)
configure_logging(logging.INFO)
logging.getLogger("werkzeug").level = logging.WARNING
# check config file exists
if not os.path.isfile(config_pathname):
logger.error("Configuration file '%s' not found.", config_pathname)
sys.exit(1)
# load and check config
config.load(config_pathname)
is_config_ok, erreur_config = config.check()
if not is_config_ok:
logger.error("Configuration incorrecte '%s'", erreur_config)
sys.exit(1)
logger.info(config)
# initialize database
database.configure(config.get(ConfigParameter.DB))
logger.info("Start Stacosys application")
# generate RSS
rss.configure(
config.get(ConfigParameter.RSS_FILE),
config.get(ConfigParameter.SITE_NAME),
config.get(ConfigParameter.SITE_PROTO),
config.get(ConfigParameter.SITE_URL),
)
rss.generate()
# configure mailer
mailer.configure_smtp(
config.get(ConfigParameter.SMTP_HOST),
config.get_int(ConfigParameter.SMTP_PORT),
config.get(ConfigParameter.SMTP_LOGIN),
config.get(ConfigParameter.SMTP_PASSWORD),
)
mailer.configure_destination(config.get(ConfigParameter.SITE_ADMIN_EMAIL))
mailer.check()
logger.info("start interfaces %s %s %s", api, form, admin)
# start Flask
app.run(
host=config.get(ConfigParameter.HTTP_HOST),
port=config.get_int(ConfigParameter.HTTP_PORT),
debug=False,
use_reloader=False,
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("config", help="config path name")
args = parser.parse_args()
stacosys_server(args.config)
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .configuration import Config
from .mail import Mailer
from .rssfeed import Rss
config = Config()
mailer = Mailer()
rss = Rss()
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import configparser
from enum import Enum
class ConfigParameter(Enum):
DB = "main.db"
LANG = "main.lang"
HTTP_HOST = "http.host"
HTTP_PORT = "http.port"
RSS_FILE = "rss.file"
SMTP_HOST = "smtp.host"
SMTP_PORT = "smtp.port"
SMTP_LOGIN = "smtp.login"
SMTP_PASSWORD = "smtp.password"
SITE_PROTO = "site.proto"
SITE_NAME = "site.name"
SITE_URL = "site.url"
SITE_ADMIN_EMAIL = "site.admin_email"
SITE_REDIRECT = "site.redirect"
WEB_USERNAME = "web.username"
WEB_PASSWORD = "web.password"
class Config:
_cfg = configparser.ConfigParser()
def load(self, config_pathname):
self._cfg.read(config_pathname)
def _split_key(self, key: ConfigParameter):
section, param = str(key.value).split(".")
if not param:
param = section
section = ""
return (section, param)
def exists(self, key: ConfigParameter):
section, param = self._split_key(key)
return self._cfg.has_option(section, param)
def get(self, key: ConfigParameter) -> str:
section, param = self._split_key(key)
return (
self._cfg.get(section, param)
if self._cfg.has_option(section, param)
else ""
)
def put(self, key: ConfigParameter, value):
section, param = self._split_key(key)
if section and not self._cfg.has_section(section):
self._cfg.add_section(section)
self._cfg.set(section, param, str(value))
def get_int(self, key: ConfigParameter) -> int:
value = self.get(key)
return int(value) if value else 0
def get_bool(self, key: ConfigParameter) -> bool:
value = self.get(key)
assert value in (
"yes",
"true",
"no",
"false",
), f"Parameètre booléen incorrect {key.value}"
return value in ("yes", "true")
def check(self):
for key in ConfigParameter:
if not self.get(key):
return (False, key.value)
return (True, None)
def __repr__(self):
dict_repr = {}
for section in self._cfg.sections():
for option in self._cfg.options(section):
dict_repr[".".join([section, option])] = self._cfg.get(section, option)
return str(dict_repr)
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import smtplib
import ssl
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
class Mailer:
def __init__(self) -> None:
self._smtp_host: str = ""
self._smtp_port: int = 0
self._smtp_login: str = ""
self._smtp_password: str = ""
self._site_admin_email: str = ""
def configure_smtp(
self,
smtp_host,
smtp_port,
smtp_login,
smtp_password,
) -> None:
self._smtp_host = smtp_host
self._smtp_port = smtp_port
self._smtp_login = smtp_login
self._smtp_password = smtp_password
def configure_destination(self, site_admin_email) -> None:
self._site_admin_email = site_admin_email
def check(self):
server = smtplib.SMTP_SSL(
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
)
server.login(self._smtp_login, self._smtp_password)
server.close()
def send(self, subject, message) -> bool:
sender = self._smtp_login
receivers = [self._site_admin_email]
msg = MIMEText(message)
msg["Subject"] = subject
msg["To"] = self._site_admin_email
msg["From"] = sender
# pylint: disable=bare-except
try:
server = smtplib.SMTP_SSL(
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
)
server.login(self._smtp_login, self._smtp_password)
server.send_message(msg, sender, receivers)
server.close()
success = True
except:
success = False
return success
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from datetime import datetime
import markdown
import PyRSS2Gen
from stacosys.db import dao
class Rss:
def __init__(self) -> None:
self._rss_file: str = ""
self._site_proto: str = ""
self._site_name: str = ""
self._site_url: str = ""
def configure(
self,
rss_file,
site_name,
site_proto,
site_url,
) -> None:
self._rss_file = rss_file
self._site_name = site_name
self._site_proto = site_proto
self._site_url = site_url
def generate(self) -> None:
markdownizer = markdown.Markdown()
items = []
for row in dao.find_recent_published_comments():
item_link = f"{self._site_proto}://{self._site_url}{row.url}"
items.append(
PyRSS2Gen.RSSItem(
title=f"{self._site_proto}://{self._site_url}{row.url} - {row.author_name}",
link=item_link,
description=markdownizer.convert(row.content),
guid=PyRSS2Gen.Guid(f"{item_link}{row.id}"),
pubDate=row.published,
)
)
rss_title = f"Commentaires du site {self._site_name}"
rss = PyRSS2Gen.RSS2(
title=rss_title,
link=f"{self._site_proto}://{self._site_url}",
description=rss_title,
lastBuildDate=datetime.now(),
items=items,
)
# pylint: disable=consider-using-with
rss.write_xml(open(self._rss_file, "w", encoding="utf-8"), encoding="utf-8")
@@ -0,0 +1,9 @@
Hi,
The comment should be published soon. It has been approved.
--
Stacosys
{{ original }}
+9
View File
@@ -0,0 +1,9 @@
Hi,
The comment will not be published. It has been dropped.
--
Stacosys
{{ original }}
+16
View File
@@ -0,0 +1,16 @@
Hi,
A new comment has been submitted for post {{ url }}
You have two choices:
- reject the comment by replying NO (or no),
- accept the comment by sending back the email as it is.
If you choose the latter option, Stacosys is going to publish the commennt.
Please find comment details below:
{{ comment }}
--
Stacosys
+1
View File
@@ -0,0 +1 @@
New comment
@@ -0,0 +1 @@
{{ site }} : comments
@@ -0,0 +1,9 @@
Bonjour,
Le commentaire sera bientôt publié. Il a été approuvé.
--
Stacosys
{{ original }}
+9
View File
@@ -0,0 +1,9 @@
Bonjour,
Le commentaire ne sera pas publié. Il a été rejeté.
--
Stacosys
{{ original }}
+16
View File
@@ -0,0 +1,16 @@
Bonjour,
Un nouveau commentaire a été posté pour l'article {{ url }}
Vous avez deux réponses possibles :
- rejeter le commentaire en répondant NO (ou no),
- accepter le commentaire en renvoyant cet email tel quel.
Si cette dernière option est choisie, Stacosys publiera le commentaire très bientôt.
Voici les détails concernant le commentaire :
{{ comment }}
--
Stacosys
+1
View File
@@ -0,0 +1 @@
Nouveau commentaire
@@ -0,0 +1 @@
{{ site }} : commentaires
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import json
import logging
import pytest
from stacosys.db import dao, database
from stacosys.interface import api, app
def init_test_db():
c1 = dao.create_comment("/site1", "Bob", "/bob.site", "", "comment 1")
c2 = dao.create_comment("/site2", "Bill", "/bill.site", "", "comment 2")
c3 = dao.create_comment("/site3", "Jack", "/jack.site", "", "comment 3")
dao.publish_comment(c1)
dao.publish_comment(c3)
assert c2
@pytest.fixture
def client():
logger = logging.getLogger(__name__)
database.configure("sqlite:memory://db.sqlite")
init_test_db()
logger.info(f"start interface {api}")
return app.test_client()
def test_api_ping(client):
resp = client.get("/api/ping")
assert resp.data == b"OK"
def test_api_count_global(client):
resp = client.get("/api/comments/count")
d = json.loads(resp.data)
assert d and d["count"] == 2
def test_api_count_url(client):
resp = client.get("/api/comments/count?url=/site1")
d = json.loads(resp.data)
assert d and d["count"] == 1
resp = client.get("/api/comments/count?url=/site2")
d = json.loads(resp.data)
assert d and d["count"] == 0
def test_api_comment(client):
resp = client.get("/api/comments?url=/site1")
d = json.loads(resp.data)
assert d and len(d["data"]) == 1
comment = d["data"][0]
assert comment["author"] == "Bob"
assert comment["content"] == "comment 1"
def test_api_comment_not_found(client):
resp = client.get("/api/comments?url=/site2")
d = json.loads(resp.data)
assert d and d["data"] == []
+42 -32
View File
@@ -2,46 +2,56 @@
# -*- coding: UTF-8 -*-
import pytest
from stacosys.conf.config import Config, ConfigParameter
from stacosys.service import config
from stacosys.service.configuration import ConfigParameter
EXPECTED_DB = "sqlite://db.sqlite"
EXPECTED_DB_SQLITE_FILE = "db.sqlite"
EXPECTED_HTTP_PORT = 8080
EXPECTED_LANG = "fr"
EXPECTED_IMAP_PORT = "5000"
EXPECTED_IMAP_LOGIN = "user"
@pytest.fixture
def init_config():
config.put(ConfigParameter.DB, EXPECTED_DB)
config.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
def conf():
conf = Config()
conf.put(ConfigParameter.DB_SQLITE_FILE, EXPECTED_DB_SQLITE_FILE)
conf.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
conf.put(ConfigParameter.IMAP_PORT, EXPECTED_IMAP_PORT)
conf.put(ConfigParameter.SMTP_STARTTLS, "yes")
conf.put(ConfigParameter.IMAP_SSL, "false")
return conf
def test_split_key():
section, param = config._split_key(ConfigParameter.HTTP_PORT)
assert section == "http" and param == "port"
def test_exists(conf):
assert conf is not None
assert conf.exists(ConfigParameter.DB_SQLITE_FILE)
assert not conf.exists(ConfigParameter.IMAP_HOST)
def test_exists(init_config):
assert config.exists(ConfigParameter.DB)
def test_get(conf):
assert conf is not None
assert conf.get(ConfigParameter.DB_SQLITE_FILE) == EXPECTED_DB_SQLITE_FILE
assert conf.get(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
assert conf.get(ConfigParameter.HTTP_HOST) is None
assert conf.get(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
assert conf.get(ConfigParameter.IMAP_PORT) == EXPECTED_IMAP_PORT
assert conf.get_int(ConfigParameter.IMAP_PORT) == int(EXPECTED_IMAP_PORT)
try:
conf.get_int(ConfigParameter.HTTP_PORT)
assert False
except Exception:
pass
assert conf.get_bool(ConfigParameter.SMTP_STARTTLS)
assert not conf.get_bool(ConfigParameter.IMAP_SSL)
try:
conf.get_bool(ConfigParameter.DB_URL)
assert False
except Exception:
pass
def test_get(init_config):
assert config.get(ConfigParameter.DB) == EXPECTED_DB
assert config.get(ConfigParameter.HTTP_HOST) == ""
assert config.get(ConfigParameter.HTTP_PORT) == str(EXPECTED_HTTP_PORT)
assert config.get_int(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
with pytest.raises(AssertionError):
config.get_bool(ConfigParameter.DB)
def test_put(init_config):
assert not config.exists(ConfigParameter.LANG)
config.put(ConfigParameter.LANG, EXPECTED_LANG)
assert config.exists(ConfigParameter.LANG)
assert config.get(ConfigParameter.LANG) == EXPECTED_LANG
def test_check(init_config):
success, error = config.check()
assert not success and error
def test_put(conf):
assert conf is not None
assert not conf.exists(ConfigParameter.IMAP_LOGIN)
conf.put(ConfigParameter.IMAP_LOGIN, EXPECTED_IMAP_LOGIN)
assert conf.exists(ConfigParameter.IMAP_LOGIN)
assert conf.get(ConfigParameter.IMAP_LOGIN) == EXPECTED_IMAP_LOGIN
-124
View File
@@ -1,124 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import time
import pytest
from stacosys.db import dao, database
from stacosys.model.comment import Comment
@pytest.fixture
def setup_db():
database.configure("sqlite:memory://db.sqlite")
def equals_comment(comment: Comment, other):
return (
comment.id == other.id
and comment.author_gravatar == other.author_gravatar
and comment.author_name == other.author_name
and comment.author_site == other.author_site
and comment.content == other.content
and comment.created == other.created
and comment.notified == other.notified
and comment.published == other.published
)
def test_find_comment_by_id(setup_db):
assert dao.find_comment_by_id(1) is None
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
assert c1.id is not None
find_c1 = dao.find_comment_by_id(c1.id)
assert find_c1
assert equals_comment(c1, find_c1)
c1.id = find_c1.id
dao.delete_comment(c1)
assert dao.find_comment_by_id(c1.id) is None
def test_dao_published(setup_db):
assert 0 == dao.count_published_comments("")
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
assert 0 == dao.count_published_comments("")
assert 1 == len(dao.find_not_published_comments())
dao.publish_comment(c1)
assert 1 == dao.count_published_comments("")
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
dao.publish_comment(c2)
assert 2 == dao.count_published_comments("")
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
dao.publish_comment(c3)
assert 0 == len(dao.find_not_published_comments())
# count published
assert 1 == dao.count_published_comments("/post1")
assert 2 == dao.count_published_comments("/post2")
# find published
assert 0 == len(dao.find_published_comments_by_url("/"))
assert 1 == len(dao.find_published_comments_by_url("/post1"))
assert 2 == len(dao.find_published_comments_by_url("/post2"))
def test_dao_notified(setup_db):
assert 0 == len(dao.find_not_notified_comments())
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
assert 1 == len(dao.find_not_notified_comments())
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
assert 2 == len(dao.find_not_notified_comments())
dao.notify_comment(c1)
dao.notify_comment(c2)
assert 0 == len(dao.find_not_notified_comments())
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
assert 1 == len(dao.find_not_notified_comments())
dao.notify_comment(c3)
assert 0 == len(dao.find_not_notified_comments())
def create_comment(url, author_name, content):
return dao.create_comment(url, author_name, "", "", content)
def test_find_recent_published_comments(setup_db):
comments = []
comments.append(create_comment("/post", "Adam", "Comment 1"))
comments.append(create_comment("/post", "Arf", "Comment 2"))
comments.append(create_comment("/post", "Arwin", "Comment 3"))
comments.append(create_comment("/post", "Bill", "Comment 4"))
comments.append(create_comment("/post", "Bo", "Comment 5"))
comments.append(create_comment("/post", "Charles", "Comment 6"))
comments.append(create_comment("/post", "Dan", "Comment 7"))
comments.append(create_comment("/post", "Dwayne", "Comment 8"))
comments.append(create_comment("/post", "Erl", "Comment 9"))
comments.append(create_comment("/post", "Jay", "Comment 10"))
comments.append(create_comment("/post", "Kenny", "Comment 11"))
comments.append(create_comment("/post", "Lord", "Comment 12"))
rows = dao.find_recent_published_comments()
assert len(rows) == 0
# publish every second
for comment in comments:
dao.publish_comment(comment)
time.sleep(1)
rows = dao.find_recent_published_comments()
assert len(rows) == 10
authors = [row.author_name for row in rows]
assert authors == [
"Lord",
"Kenny",
"Jay",
"Erl",
"Dwayne",
"Dan",
"Charles",
"Bo",
"Bill",
"Arwin",
]
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import logging
import pytest
from stacosys.db import database
from stacosys.interface import app, form
@pytest.fixture
def client():
logger = logging.getLogger(__name__)
database.configure("sqlite:memory://db.sqlite")
logger.info(f"start interface {form}")
return app.test_client()
def test_new_comment_honeypot(client):
resp = client.post(
"/newcomment", content_type="multipart/form-data", data={"remarque": "trapped"}
)
assert resp.status == "400 BAD REQUEST"
def test_new_comment_success(client):
resp = client.post(
"/newcomment",
content_type="multipart/form-data",
data={"author": "Jack", "url": "/site3", "message": "comment 3"},
)
assert resp.status == "302 FOUND"
def test_check_form_data():
from stacosys.interface.form import check_form_data
assert check_form_data({"author": "Jack", "url": "/site3", "message": "comment 3"})
assert not check_form_data(
{"author": "Jack", "url": "/site3", "message": "comment 3", "extra": "ball"}
)
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import pytest
from stacosys.service import mailer
def test_configure_and_check():
mailer.configure_smtp("localhost", 2525, "admin", "admin")
mailer.configure_destination("admin@mydomain.com")
with pytest.raises(ConnectionRefusedError):
mailer.check()
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from stacosys.service import rss
def test_configure():
rss.configure("comments.xml", "blog", "http", "blog.mydomain.com")
+5
View File
@@ -0,0 +1,5 @@
from stacosys import __version__
def test_version():
assert __version__ == "2.0"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
from stacosys.core.templater import Templater, Template
def get_template_content(lang, template_name, **kwargs):
current_path = os.path.dirname(__file__)
template_path = os.path.abspath(os.path.join(current_path, "../stacosys/templates"))
templater = Templater(template_path)
template = templater.get_template(lang, template_name)
assert template
return template.render(kwargs)
def test_approve_comment():
content = get_template_content("fr", Template.APPROVE_COMMENT, original="[texte]")
assert content.startswith("Bonjour,\n\nLe commentaire sera bientôt publié.")
assert content.endswith("[texte]")
content = get_template_content("en", Template.APPROVE_COMMENT, original="[texte]")
assert content.startswith("Hi,\n\nThe comment should be published soon.")
assert content.endswith("[texte]")
def test_drop_comment():
content = get_template_content("fr", Template.DROP_COMMENT, original="[texte]")
assert content.startswith("Bonjour,\n\nLe commentaire ne sera pas publié.")
assert content.endswith("[texte]")
content = get_template_content("en", Template.DROP_COMMENT, original="[texte]")
assert content.startswith("Hi,\n\nThe comment will not be published.")
assert content.endswith("[texte]")
def test_new_comment():
content = get_template_content("fr", Template.NEW_COMMENT, comment="[comment]")
assert content.startswith("Bonjour,\n\nUn nouveau commentaire a été posté")
assert content.endswith("[comment]\n\n--\nStacosys")
content = get_template_content("en", Template.NEW_COMMENT, comment="[comment]")
assert content.startswith("Hi,\n\nA new comment has been submitted")
assert content.endswith("[comment]\n\n--\nStacosys")
def test_notify_message():
content = get_template_content("fr", Template.NOTIFY_MESSAGE)
assert content == "Nouveau commentaire"
content = get_template_content("en", Template.NOTIFY_MESSAGE)
assert content == "New comment"
def test_rss_title():
content = get_template_content("fr", Template.RSS_TITLE_MESSAGE, site="[site]")
assert content == "[site] : commentaires"
content = get_template_content("en", Template.RSS_TITLE_MESSAGE, site="[site]")
assert content == "[site] : comments"
-1
View File
@@ -1 +0,0 @@
RSS