improve encapsulation

pull/6/head
Yax 4 years ago
parent 6c855e7ead
commit adc6451116

@ -1,19 +1,21 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import sys
import os
import argparse import argparse
import logging import logging
import os
import sys
from flask import Flask from flask import Flask
from flask_apscheduler import APScheduler
import stacosys.conf.config as config import stacosys.conf.config as config
from stacosys.core import database from stacosys.core import database
from stacosys.core import rss from stacosys.core.rss import Rss
#from stacosys.interface import api from stacosys.core.mailer import Mailer
#from stacosys.interface import form from stacosys.interface import app
from stacosys.interface import api
from stacosys.interface import form
from stacosys.interface import scheduler
# configure logging # configure logging
def configure_logging(level): def configure_logging(level):
@ -29,33 +31,8 @@ def configure_logging(level):
root_logger.addHandler(ch) root_logger.addHandler(ch)
class JobConfig(object):
JOBS = []
SCHEDULER_EXECUTORS = {"default": {"type": "threadpool", "max_workers": 4}}
def __init__(self, imap_polling_seconds, new_comment_polling_seconds):
self.JOBS = [
{
"id": "fetch_mail",
"func": "stacosys.core.cron:fetch_mail_answers",
"trigger": "interval",
"seconds": imap_polling_seconds,
},
{
"id": "submit_new_comment",
"func": "stacosys.core.cron:submit_new_comment",
"trigger": "interval",
"seconds": new_comment_polling_seconds,
},
]
def stacosys_server(config_pathname): def stacosys_server(config_pathname):
app = Flask(__name__)
conf = config.Config.load(config_pathname) conf = config.Config.load(config_pathname)
# configure logging # configure logging
@ -68,26 +45,38 @@ def stacosys_server(config_pathname):
db = database.Database() db = database.Database()
db.setup(conf.get(config.DB_URL)) db.setup(conf.get(config.DB_URL))
# cron email fetcher
app.config.from_object(
JobConfig(
conf.get_int(config.IMAP_POLLING), conf.get_int(config.COMMENT_POLLING)
)
)
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
logger.info("Start Stacosys application") logger.info("Start Stacosys application")
# generate RSS for all sites # generate RSS for all sites
rss_manager = rss.Rss(conf.get(config.LANG), conf.get(config.RSS_FILE), conf.get(config.RSS_PROTO)) rss = Rss(
rss_manager.generate_all() conf.get(config.LANG), conf.get(config.RSS_FILE), conf.get(config.RSS_PROTO)
)
rss.generate_all()
# configure mailer
mailer = Mailer(
conf.get(config.IMAP_HOST),
conf.get_int(config.IMAP_PORT),
conf.get_bool(config.IMAP_SSL),
conf.get(config.IMAP_LOGIN),
conf.get(config.IMAP_PASSWORD),
conf.get(config.SMTP_HOST),
conf.get_int(config.SMTP_PORT),
conf.get_bool(config.SMTP_STARTTLS),
conf.get(config.SMTP_LOGIN),
conf.get(config.SMTP_PASSWORD),
)
# start Flask # configure scheduler
#logger.info("Load interface %s" % api) scheduler.configure(
#logger.info("Load interface %s" % form) conf.get_int(config.IMAP_POLLING),
conf.get_int(config.COMMENT_POLLING),
conf.get(config.LANG),
mailer,
rss,
)
# start Flask
app.run( app.run(
host=conf.get(config.HTTP_HOST), host=conf.get(config.HTTP_HOST),
port=conf.get(config.HTTP_PORT), port=conf.get(config.HTTP_PORT),

@ -6,7 +6,7 @@ import re
import time import time
from datetime import datetime from datetime import datetime
from stacosys.core import mailer, rss from stacosys.core import rss
from stacosys.core.templater import get_template from stacosys.core.templater import get_template
from stacosys.model.comment import Comment, Site from stacosys.model.comment import Comment, Site
from stacosys.model.email import Email from stacosys.model.email import Email
@ -14,59 +14,18 @@ from stacosys.model.email import Email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def cron(func): def fetch_mail_answers(lang, mailer, rss):
def wrapper():
logger.debug('execute CRON ' + func.__name__)
func()
return wrapper
@cron
def fetch_mail_answers():
for msg in mailer.fetch(): for msg in mailer.fetch():
if re.search(r'.*STACOSYS.*\[(\d+)\:(\w+)\]', msg.subject, re.DOTALL): if re.search(r".*STACOSYS.*\[(\d+)\:(\w+)\]", msg.subject, re.DOTALL):
if _reply_comment_email(msg): if _reply_comment_email(lang, mailer, rss, msg):
mailer.delete(msg.id) mailer.delete(msg.id)
@cron def _reply_comment_email(lang, mailer, rss, email: Email):
def submit_new_comment():
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 = get_template('new_comment').render(
url=comment.url, comment=comment_text
)
# send email
site = Site.get(Site.id == comment.site)
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)
def _reply_comment_email(email : Email):
m = re.search(r'\[(\d+)\:(\w+)\]', email.subject) m = re.search(r"\[(\d+)\:(\w+)\]", email.subject)
if not m: if not m:
logger.warn('ignore corrupted email. No token %s' % email.subject) logger.warn("ignore corrupted email. No token %s" % email.subject)
return return
comment_id = int(m.group(1)) comment_id = int(m.group(1))
token = m.group(2) token = m.group(2)
@ -75,39 +34,75 @@ def _reply_comment_email(email : Email):
try: try:
comment = Comment.select().where(Comment.id == comment_id).get() comment = Comment.select().where(Comment.id == comment_id).get()
except: except:
logger.warn('unknown comment %d' % comment_id) logger.warn("unknown comment %d" % comment_id)
return True return True
if comment.published: if comment.published:
logger.warn('ignore already published email. token %d' % comment_id) logger.warn("ignore already published email. token %d" % comment_id)
return return
if comment.site.token != token: if comment.site.token != token:
logger.warn('ignore corrupted email. Unknown token %d' % comment_id) logger.warn("ignore corrupted email. Unknown token %d" % comment_id)
return return
if not email.plain_text_content: if not email.plain_text_content:
logger.warn('ignore empty email') logger.warn("ignore empty email")
return return
# safe logic: no answer or unknown answer is a go for publishing # safe logic: no answer or unknown answer is a go for publishing
if email.plain_text_content[:2].upper() in ('NO'): if email.plain_text_content[:2].upper() in ("NO"):
logger.info('discard comment: %d' % comment_id) logger.info("discard comment: %d" % comment_id)
comment.delete_instance() comment.delete_instance()
new_email_body = get_template('drop_comment').render(original=email.plain_text_content) new_email_body = get_template(lang, "drop_comment").render(
if not mailer.send(email.from_addr, 'Re: ' + email.subject, new_email_body): original=email.plain_text_content
logger.warn('minor failure. cannot send rejection mail ' + email.subject) )
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
logger.warn("minor failure. cannot send rejection mail " + email.subject)
else: else:
# save publishing datetime # save publishing datetime
comment.publish() comment.publish()
logger.info('commit comment: %d' % comment_id) logger.info("commit comment: %d" % comment_id)
# rebuild RSS # rebuild RSS
rss.generate_site(token) rss.generate_site(token)
# send approval confirmation email to admin # send approval confirmation email to admin
new_email_body = get_template('approve_comment').render(original=email.plain_text_content) new_email_body = get_template(lang, "approve_comment").render(
if not mailer.send(email.from_addr, 'Re: ' + email.subject, new_email_body): original=email.plain_text_content
logger.warn('minor failure. cannot send approval email ' + email.subject) )
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 return True
def submit_new_comment(lang, 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)
# TODO use constants for template names
email_body = get_template(lang, "new_comment").render(
url=comment.url, comment=comment_text
)
# send email
site = Site.get(Site.id == comment.site)
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)

@ -15,53 +15,75 @@ from stacosys.model.email import Email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _open_mailbox(): class Mailer:
return imap.Mailbox( def __init__(
config.get(config.IMAP_HOST), self,
config.get_int(config.IMAP_PORT), imap_host,
config.get_bool(config.IMAP_SSL), imap_port,
config.get(config.IMAP_LOGIN), imap_ssl,
config.get(config.IMAP_PASSWORD), imap_login,
) imap_password,
smtp_host,
smtp_port,
smtp_starttls,
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_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(): def fetch(self):
msgs = [] msgs = []
try: try:
with _open_mailbox() as mbox: with self._open_mailbox() as mbox:
count = mbox.get_count() count = mbox.get_count()
for num in range(count): for num in range(count):
msgs.append(mbox.fetch_message(num + 1)) msgs.append(mbox.fetch_message(num + 1))
except: except:
logger.exception("fetch mail exception") logger.exception("fetch mail exception")
return msgs return msgs
def send(self, to_email, subject, message):
def send(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
# Create the container (outer) email message. success = True
msg = MIMEText(message) try:
msg["Subject"] = subject s = smtplib.SMTP(self._smtp_host, self._smtp_port)
msg["To"] = to_email if self._smtp_starttls:
msg["From"] = config.get(config.SMTP_LOGIN) s.starttls()
s.login(self._smtp_login, self._smtp_password)
s.send_message(msg)
s.quit()
except:
logger.exception("send mail exception")
success = False
return success
success = True def delete(self, id):
try: try:
s = smtplib.SMTP(config.get(config.SMTP_HOST), config.get_int(config.SMTP_PORT)) with self._open_mailbox() as mbox:
if config.get_bool(config.SMTP_STARTTLS): mbox.delete_message(id)
s.starttls() except:
s.login(config.get(config.SMTP_LOGIN), config.get(config.SMTP_PASSWORD)) logger.exception("delete mail exception")
s.send_message(msg)
s.quit()
except:
logger.exception("send mail exception")
success = False
return success
def delete(id):
try:
with _open_mailbox() as mbox:
mbox.delete_message(id)
except:
logger.exception("delete mail exception")

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask
app = Flask(__name__)

@ -2,31 +2,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
from flask import abort, jsonify, request from flask import abort, jsonify, request
from stacosys.conf import config from stacosys.interface import app
from stacosys.model.comment import Comment from stacosys.model.comment import Comment
from stacosys.model.site import Site from stacosys.model.site import Site
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = config.flaskapp()
@app.route('/ping', methods=['GET']) @app.route("/ping", methods=["GET"])
def ping(): def ping():
return 'OK' return "OK"
@app.route('/comments', methods=['GET']) @app.route("/comments", methods=["GET"])
def query_comments(): def query_comments():
comments = [] comments = []
try: try:
token = request.args.get('token', '') token = request.args.get("token", "")
url = request.args.get('url', '') url = request.args.get("url", "")
logger.info('retrieve comments for url %s' % (url)) logger.info("retrieve comments for url %s" % (url))
for comment in ( for comment in (
Comment.select(Comment) Comment.select(Comment)
.join(Site) .join(Site)
@ -38,29 +36,29 @@ def query_comments():
.order_by(+Comment.published) .order_by(+Comment.published)
): ):
d = { d = {
'author': comment.author_name, "author": comment.author_name,
'content': comment.content, "content": comment.content,
'avatar': comment.author_gravatar, "avatar": comment.author_gravatar,
'date': comment.published.strftime('%Y-%m-%d %H:%M:%S') "date": comment.published.strftime("%Y-%m-%d %H:%M:%S"),
} }
if comment.author_site: if comment.author_site:
d['site'] = comment.author_site d["site"] = comment.author_site
logger.debug(d) logger.debug(d)
comments.append(d) comments.append(d)
r = jsonify({'data': comments}) r = jsonify({"data": comments})
r.status_code = 200 r.status_code = 200
except: except:
logger.warn('bad request') logger.warn("bad request")
r = jsonify({'data': []}) r = jsonify({"data": []})
r.status_code = 400 r.status_code = 400
return r return r
@app.route('/comments/count', methods=['GET']) @app.route("/comments/count", methods=["GET"])
def get_comments_count(): def get_comments_count():
try: try:
token = request.args.get('token', '') token = request.args.get("token", "")
url = request.args.get('url', '') url = request.args.get("url", "")
count = ( count = (
Comment.select(Comment) Comment.select(Comment)
.join(Site) .join(Site)
@ -71,9 +69,9 @@ def get_comments_count():
) )
.count() .count()
) )
r = jsonify({'count': count}) r = jsonify({"count": count})
r.status_code = 200 r.status_code = 200
except: except:
r = jsonify({'count': 0}) r = jsonify({"count": 0})
r.status_code = 200 r.status_code = 200
return r return r

@ -3,53 +3,51 @@
import logging import logging
from datetime import datetime from datetime import datetime
from flask import abort, redirect, request from flask import abort, redirect, request
from stacosys.conf import config from stacosys.interface import app
from stacosys.model.comment import Comment from stacosys.model.comment import Comment
from stacosys.model.site import Site from stacosys.model.site import Site
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = config.flaskapp()
@app.route('/newcomment', methods=['POST']) @app.route("/newcomment", methods=["POST"])
def new_form_comment(): def new_form_comment():
try: try:
data = request.form data = request.form
logger.info('form data ' + str(data)) logger.info("form data " + str(data))
# validate token: retrieve site entity # validate token: retrieve site entity
token = data.get('token', '') token = data.get("token", "")
site = Site.select().where(Site.token == token).get() site = Site.select().where(Site.token == token).get()
if site is None: if site is None:
logger.warn('Unknown site %s' % token) logger.warn("Unknown site %s" % token)
abort(400) abort(400)
# honeypot for spammers # honeypot for spammers
captcha = data.get('remarque', '') captcha = data.get("remarque", "")
if captcha: if captcha:
logger.warn('discard spam: data %s' % data) logger.warn("discard spam: data %s" % data)
abort(400) abort(400)
url = data.get('url', '') url = data.get("url", "")
author_name = data.get('author', '').strip() author_name = data.get("author", "").strip()
author_gravatar = data.get('email', '').strip() author_gravatar = data.get("email", "").strip()
author_site = data.get('site', '').lower().strip() author_site = data.get("site", "").lower().strip()
if author_site and author_site[:4] != 'http': if author_site and author_site[:4] != "http":
author_site = 'http://' + author_site author_site = "http://" + author_site
message = data.get('message', '') message = data.get("message", "")
# anti-spam again # anti-spam again
if not url or not author_name or not message: if not url or not author_name or not message:
logger.warn('empty field: data %s' % data) logger.warn("empty field: data %s" % data)
abort(400) abort(400)
check_form_data(data) check_form_data(data)
# add a row to Comment table # add a row to Comment table
created = datetime.now().strftime('%Y-%m-%d %H:%M:%S') created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
comment = Comment( comment = Comment(
site=site, site=site,
url=url, url=url,
@ -64,18 +62,18 @@ def new_form_comment():
comment.save() comment.save()
except: except:
logger.exception('new comment failure') logger.exception("new comment failure")
abort(400) abort(400)
return redirect('/redirect/', code=302) return redirect("/redirect/", code=302)
def check_form_data(data): def check_form_data(data):
fields = ['url', 'message', 'site', 'remarque', 'author', 'token', 'email'] fields = ["url", "message", "site", "remarque", "author", "token", "email"]
d = data.to_dict() d = data.to_dict()
for field in fields: for field in fields:
if field in d: if field in d:
del d[field] del d[field]
if d: if d:
logger.warn('additional field: data %s' % data) logger.warn("additional field: data %s" % data)
abort(400) abort(400)

@ -0,0 +1,37 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_apscheduler import APScheduler
from stacosys.interface import app
class JobConfig(object):
JOBS = []
SCHEDULER_EXECUTORS = {"default": {"type": "threadpool", "max_workers": 4}}
def __init__(self, imap_polling_seconds, new_comment_polling_seconds, lang, mailer, rss):
self.JOBS = [
{
"id": "fetch_mail",
"func": "stacosys.core.cron:fetch_mail_answers",
"args": [lang, mailer, rss],
"trigger": "interval",
"seconds": imap_polling_seconds,
},
{
"id": "submit_new_comment",
"func": "stacosys.core.cron:submit_new_comment",
"args": [lang, mailer],
"trigger": "interval",
"seconds": new_comment_polling_seconds,
},
]
def configure(imap_polling, comment_polling, lang, mailer, rss):
app.config.from_object(JobConfig(imap_polling, comment_polling, lang, mailer, rss))
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
Loading…
Cancel
Save