WIP
This commit is contained in:
parent
ed2a284102
commit
0a2cdbbe8f
17 changed files with 197 additions and 272 deletions
|
@ -38,6 +38,12 @@ Stacosys can be hosted on the same server or on a different server than the blog
|
|||
- [Peewee ORM](http://docs.peewee-orm.com)
|
||||
- [Markdown](http://daringfireball.net/projects/markdown)
|
||||
|
||||
### Installation
|
||||
|
||||
Python 3.7
|
||||
|
||||
pip libs: flask peewee pyrss2gen markdown clize flask-apscheduler profig
|
||||
|
||||
### Ways of improvement
|
||||
|
||||
Current version of Stacosys fits my needs and it serves comments on [my blog](https://blogduyax.madyanne.fr). However Stacosys has been designed to serve several blogs and e-mail can be a constraint for some people. So an area of improvement would be to add an administration UI to configure sites, approve or reject comments, keep track of usage statistics and get rid of e-mails. I encourage you to fork the project and create such improvements if you need them.
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
|
@ -1,3 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import profig
|
||||
|
||||
# constants
|
||||
FLASK_APP = "flask.app"
|
||||
|
||||
DB_URL = "main.db_url"
|
||||
|
||||
HTTP_HOST = "http.host"
|
||||
HTTP_PORT = "http.port"
|
||||
|
||||
SECURITY_SALT = "security.salt"
|
||||
SECURITY_SECRET = "security.secret"
|
||||
|
||||
MAIL_POLLING = "polling.newmail"
|
||||
COMMENT_POLLING = "polling.newcomment"
|
||||
|
||||
# variable
|
||||
params = dict()
|
||||
|
||||
|
||||
def initialize(config_pathname, flask_app):
|
||||
cfg = profig.Config(config_pathname)
|
||||
cfg.sync()
|
||||
params.update(cfg)
|
||||
params.update({FLASK_APP: flask_app})
|
||||
|
||||
|
||||
def get(key):
|
||||
return params[key]
|
||||
|
||||
|
||||
def getInt(key):
|
||||
return int(params[key])
|
||||
|
||||
|
||||
def _str2bool(v):
|
||||
return v.lower() in ("yes", "true", "t", "1")
|
||||
|
||||
|
||||
def getBool(key):
|
||||
return _str2bool(params[key])
|
||||
|
||||
|
||||
def flaskapp():
|
||||
return params[FLASK_APP]
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Created with https://app.quicktype.io
|
||||
# name: stacosys
|
||||
|
||||
json_schema = """
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema#",
|
||||
"$ref": "#/definitions/Welcome",
|
||||
"definitions": {
|
||||
"Welcome": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"general": {
|
||||
"$ref": "#/definitions/General"
|
||||
},
|
||||
"http": {
|
||||
"$ref": "#/definitions/HTTP"
|
||||
},
|
||||
"security": {
|
||||
"$ref": "#/definitions/Security"
|
||||
},
|
||||
"rss": {
|
||||
"$ref": "#/definitions/RSS"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"general",
|
||||
"http",
|
||||
"rss",
|
||||
"security"
|
||||
],
|
||||
"title": "Welcome"
|
||||
},
|
||||
"General": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"debug": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lang": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"db_url",
|
||||
"debug",
|
||||
"lang"
|
||||
],
|
||||
"title": "General"
|
||||
},
|
||||
"HTTP": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"root_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"root_url"
|
||||
],
|
||||
"title": "HTTP"
|
||||
},
|
||||
"RSS": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"proto": {
|
||||
"type": "string"
|
||||
},
|
||||
"file": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file",
|
||||
"proto"
|
||||
],
|
||||
"title": "RSS"
|
||||
},
|
||||
"Security": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"salt": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"salt",
|
||||
"secret"
|
||||
],
|
||||
"title": "Security"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from flask import Flask
|
||||
from conf import config
|
||||
from jsonschema import validate
|
||||
from flask_apscheduler import APScheduler
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# add current path and parent path to syspath
|
||||
current_path = os.path.dirname(__file__)
|
||||
parent_path = os.path.abspath(os.path.join(current_path, os.path.pardir))
|
||||
paths = [current_path, parent_path]
|
||||
for path in paths:
|
||||
if path not in sys.path:
|
||||
sys.path.insert(0, path)
|
||||
|
||||
# more imports
|
||||
import database
|
||||
import processor
|
||||
from interface import api
|
||||
from interface import form
|
||||
|
||||
# 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)
|
||||
|
||||
logging_level = (20, 10)[config.general['debug']]
|
||||
configure_logging(logging_level)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Config(object):
|
||||
JOBS = [
|
||||
{
|
||||
'id': 'fetch_mail',
|
||||
'func': 'core.cron:fetch_mail_answers',
|
||||
'trigger': 'interval',
|
||||
'seconds': 120
|
||||
},
|
||||
{
|
||||
'id': 'submit_new_comment',
|
||||
'func': 'core.cron:submit_new_comment',
|
||||
'trigger': 'interval',
|
||||
'seconds': 60
|
||||
},
|
||||
]
|
||||
|
||||
# initialize database
|
||||
database.setup()
|
||||
|
||||
# start processor
|
||||
template_path = os.path.abspath(os.path.join(current_path, '../templates'))
|
||||
processor.start(template_path)
|
||||
|
||||
# cron
|
||||
app.config.from_object(Config())
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
# tune logging level
|
||||
if not config.general['debug']:
|
||||
logging.getLogger('werkzeug').level = logging.WARNING
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
|
||||
app.run(host=config.http['host'],
|
||||
port=config.http['port'],
|
||||
debug=config.general['debug'], use_reloader=False)
|
|
@ -3,26 +3,29 @@
|
|||
|
||||
import logging
|
||||
import time
|
||||
from core import app
|
||||
from core import processor
|
||||
from models.comment import Comment
|
||||
from core import mailer
|
||||
from core import templater
|
||||
from model.comment import Comment
|
||||
from model.comment import Site
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch_mail_answers():
|
||||
|
||||
logger.info('DEBUT POP MAIL')
|
||||
logger.info("DEBUT POP MAIL")
|
||||
time.sleep(80)
|
||||
logger.info('FIN POP MAIL')
|
||||
#data = request.get_json()
|
||||
#logger.debug(data)
|
||||
logger.info("FIN POP MAIL")
|
||||
# data = request.get_json()
|
||||
# logger.debug(data)
|
||||
|
||||
# processor.enqueue({'request': 'new_mail', 'data': data})
|
||||
|
||||
#processor.enqueue({'request': 'new_mail', 'data': data})
|
||||
|
||||
def submit_new_comment():
|
||||
|
||||
for comment in Comment.select().where(Comment.notified.is_null()):
|
||||
# render email body template
|
||||
|
||||
comment_list = (
|
||||
"author: %s" % comment.author_name,
|
||||
"site: %s" % comment.author_site,
|
||||
|
@ -33,15 +36,10 @@ def submit_new_comment():
|
|||
"",
|
||||
)
|
||||
comment_text = "\n".join(comment_list)
|
||||
email_body = get_template("new_comment").render(url=url, comment=comment_text)
|
||||
email_body = templater.get_template("new_comment").render(url=comment.url, comment=comment_text)
|
||||
|
||||
if clientip:
|
||||
client_ips[comment.id] = clientip
|
||||
|
||||
# send email
|
||||
subject = "STACOSYS %s: [%d:%s]" % (site.name, comment.id, token)
|
||||
mailer.send_mail(site.admin_email, subject, email_body)
|
||||
logger.debug("new comment processed ")
|
||||
|
||||
def get_template(name):
|
||||
return env.get_template(config.general["lang"] + "/" + name + ".tpl")
|
||||
site = Site.select().where(Site.id == Comment.site).get()
|
||||
# send email
|
||||
subject = "STACOSYS %s: [%d:%s]" % (site.name, comment.id, site.token)
|
||||
mailer.send_mail(site.admin_email, subject, email_body)
|
||||
logger.debug("new comment processed ")
|
||||
|
|
|
@ -2,26 +2,15 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from conf import config
|
||||
import functools
|
||||
from playhouse.db_url import connect
|
||||
|
||||
|
||||
def get_db():
|
||||
return connect(config.general['db_url'])
|
||||
return connect(config.get(config.DB_URL))
|
||||
|
||||
|
||||
def provide_db(func):
|
||||
def setup():
|
||||
from model.site import Site
|
||||
from model.comment import Comment
|
||||
|
||||
@functools.wraps(func)
|
||||
def new_function(*args, **kwargs):
|
||||
return func(get_db(), *args, **kwargs)
|
||||
|
||||
return new_function
|
||||
|
||||
|
||||
@provide_db
|
||||
def setup(db):
|
||||
from models.site import Site
|
||||
from models.comment import Comment
|
||||
|
||||
db.create_tables([Site, Comment], safe=True)
|
||||
get_db().create_tables([Site, Comment], safe=True)
|
||||
|
|
|
@ -10,10 +10,8 @@ import json
|
|||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from queue import Queue
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from models.site import Site
|
||||
from models.comment import Comment
|
||||
from model.site import Site
|
||||
from model.comment import Comment
|
||||
from helpers.hashing import md5
|
||||
from conf import config
|
||||
from core import mailer
|
||||
|
|
16
app/core/templater.py
Normal file
16
app/core/templater.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from conf import config
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
|
||||
env = Environment(loader=FileSystemLoader(template_path))
|
||||
|
||||
|
||||
def get_template(name):
|
||||
return env.get_template(config.general["lang"] + "/" + name + ".tpl")
|
|
@ -6,7 +6,7 @@ from conf import config
|
|||
|
||||
|
||||
def salt(value):
|
||||
string = '%s%s' % (value, config.security['salt'])
|
||||
string = "%s%s" % (value, config.get(config.SECURITY_SALT))
|
||||
dk = hashlib.sha256(string.encode())
|
||||
return dk.hexdigest()
|
||||
|
||||
|
|
0
app/interface/__init__.py
Normal file
0
app/interface/__init__.py
Normal file
|
@ -3,13 +3,13 @@
|
|||
|
||||
import logging
|
||||
from flask import request, jsonify, abort
|
||||
from core import app
|
||||
from models.site import Site
|
||||
from models.comment import Comment
|
||||
from model.site import Site
|
||||
from model.comment import Comment
|
||||
from conf import config
|
||||
from core import processor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = config.flaskapp()
|
||||
|
||||
@app.route("/ping", methods=['GET'])
|
||||
def ping():
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from flask import request, abort, redirect
|
||||
from core import app
|
||||
from models.site import Site
|
||||
from models.comment import Comment
|
||||
from model.site import Site
|
||||
from model.comment import Comment
|
||||
from conf import config
|
||||
from helpers.hashing import md5
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = config.flaskapp()
|
||||
|
||||
@app.route("/newcomment", methods=["POST"])
|
||||
def new_form_comment():
|
||||
|
|
|
@ -6,7 +6,7 @@ from peewee import CharField
|
|||
from peewee import TextField
|
||||
from peewee import DateTimeField
|
||||
from peewee import ForeignKeyField
|
||||
from models.site import Site
|
||||
from model.site import Site
|
||||
from core.database import get_db
|
||||
|
||||
|
94
app/run.py
94
app/run.py
|
@ -1,36 +1,90 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
from clize import Clize, run
|
||||
from jsonschema import validate
|
||||
from conf import config, schema
|
||||
from flask import Flask
|
||||
from flask_apscheduler import APScheduler
|
||||
from conf import config
|
||||
|
||||
# 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 load_json(filename):
|
||||
jsondoc = None
|
||||
with open(filename, 'rt') as json_file:
|
||||
jsondoc = json.loads(json_file.read())
|
||||
return jsondoc
|
||||
class JobConfig(object):
|
||||
|
||||
JOBS = []
|
||||
|
||||
def __init__(self, mail_polling_seconds, new_comment_polling_seconds):
|
||||
self.JOBS = [
|
||||
{
|
||||
"id": "fetch_mail",
|
||||
"func": "core.cron:fetch_mail_answers",
|
||||
"trigger": "interval",
|
||||
"seconds": mail_polling_seconds,
|
||||
},
|
||||
{
|
||||
"id": "submit_new_comment",
|
||||
"func": "core.cron:submit_new_comment",
|
||||
"trigger": "interval",
|
||||
"seconds": new_comment_polling_seconds,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@Clize
|
||||
def stacosys_server(config_pathname):
|
||||
|
||||
# load and validate startup config
|
||||
conf = load_json(config_pathname)
|
||||
json_schema = json.loads(schema.json_schema)
|
||||
validate(conf, json_schema)
|
||||
app = Flask(__name__)
|
||||
config.initialize(config_pathname, app)
|
||||
|
||||
# set configuration
|
||||
config.general = conf['general']
|
||||
config.http = conf['http']
|
||||
config.security = conf['security']
|
||||
config.rss = conf['rss']
|
||||
# configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
configure_logging(logging.INFO)
|
||||
logging.getLogger("werkzeug").level = logging.WARNING
|
||||
|
||||
# start application
|
||||
from core import app
|
||||
# initialize database
|
||||
from core import database
|
||||
|
||||
if __name__ == '__main__':
|
||||
database.setup()
|
||||
|
||||
# start processor
|
||||
from core import processor
|
||||
|
||||
# cron email fetcher
|
||||
app.config.from_object(
|
||||
JobConfig(
|
||||
config.getInt(config.MAIL_POLLING), config.getInt(config.COMMENT_POLLING)
|
||||
)
|
||||
)
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
|
||||
# start Flask
|
||||
from interface import api
|
||||
from interface import form
|
||||
|
||||
app.run(
|
||||
host=config.get(config.HTTP_HOST),
|
||||
port=config.get(config.HTTP_PORT),
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(stacosys_server)
|
||||
|
|
21
config.ini
Executable file
21
config.ini
Executable file
|
@ -0,0 +1,21 @@
|
|||
; Default configuration
|
||||
[main]
|
||||
lang = fr
|
||||
db_url = sqlite:///db.sqlite
|
||||
|
||||
[http]
|
||||
root_url = http://localhost:8100
|
||||
host = 0.0.0.0
|
||||
port = 8100
|
||||
|
||||
[security]
|
||||
salt = BRRJRqXgGpXWrgTidBPcixIThHpDuKc0
|
||||
secret = Uqca5Kc8xuU6THz9
|
||||
|
||||
[rss]
|
||||
proto = http
|
||||
file = comments.xml
|
||||
|
||||
[polling]
|
||||
newmail = 15
|
||||
newcomment = 60
|
Loading…
Add table
Reference in a new issue