WIP
This commit is contained in:
parent
0a2cdbbe8f
commit
9146588902
11 changed files with 185 additions and 177 deletions
|
@ -7,6 +7,7 @@ import profig
|
||||||
FLASK_APP = "flask.app"
|
FLASK_APP = "flask.app"
|
||||||
|
|
||||||
DB_URL = "main.db_url"
|
DB_URL = "main.db_url"
|
||||||
|
LANG = "main.lang"
|
||||||
|
|
||||||
HTTP_HOST = "http.host"
|
HTTP_HOST = "http.host"
|
||||||
HTTP_PORT = "http.port"
|
HTTP_PORT = "http.port"
|
||||||
|
@ -14,8 +15,13 @@ HTTP_PORT = "http.port"
|
||||||
SECURITY_SALT = "security.salt"
|
SECURITY_SALT = "security.salt"
|
||||||
SECURITY_SECRET = "security.secret"
|
SECURITY_SECRET = "security.secret"
|
||||||
|
|
||||||
MAIL_POLLING = "polling.newmail"
|
RSS_PROTO = "rss.proto"
|
||||||
COMMENT_POLLING = "polling.newcomment"
|
RSS_FILE = "rss.file"
|
||||||
|
|
||||||
|
MAIL_POLLING = "mail.fetch_polling"
|
||||||
|
COMMENT_POLLING = "main.newcomment_polling"
|
||||||
|
MAILER_URL = "mail.mailer_url"
|
||||||
|
|
||||||
|
|
||||||
# variable
|
# variable
|
||||||
params = dict()
|
params = dict()
|
||||||
|
|
105
app/core/cron.py
105
app/core/cron.py
|
@ -2,30 +2,48 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from core import mailer
|
from core import mailer
|
||||||
from core import templater
|
from core.templater import get_template
|
||||||
|
from core import rss
|
||||||
from model.comment import Comment
|
from model.comment import Comment
|
||||||
from model.comment import Site
|
from model.comment import Site
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
client_ips = {}
|
||||||
|
|
||||||
|
|
||||||
|
def cron(func):
|
||||||
|
def wrapper():
|
||||||
|
logger.debug("execute fun " + func)
|
||||||
|
func()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@cron
|
||||||
def fetch_mail_answers():
|
def fetch_mail_answers():
|
||||||
|
|
||||||
logger.info("DEBUT POP MAIL")
|
msg = {}
|
||||||
time.sleep(80)
|
|
||||||
logger.info("FIN POP MAIL")
|
if msg["request"] == "new_mail":
|
||||||
|
reply_comment_email(msg["data"])
|
||||||
|
mailer.delete(msg["data"])
|
||||||
|
|
||||||
# data = request.get_json()
|
# data = request.get_json()
|
||||||
# logger.debug(data)
|
# logger.debug(data)
|
||||||
|
|
||||||
# processor.enqueue({'request': 'new_mail', 'data': data})
|
# processor.enqueue({'request': 'new_mail', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
@cron
|
||||||
def submit_new_comment():
|
def submit_new_comment():
|
||||||
|
|
||||||
for comment in Comment.select().where(Comment.notified.is_null()):
|
for comment in Comment.select().where(Comment.notified.is_null()):
|
||||||
|
|
||||||
comment_list = (
|
comment_list = (
|
||||||
"author: %s" % comment.author_name,
|
"author: %s" % comment.author_name,
|
||||||
"site: %s" % comment.author_site,
|
"site: %s" % comment.author_site,
|
||||||
|
@ -36,10 +54,83 @@ def submit_new_comment():
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
comment_text = "\n".join(comment_list)
|
comment_text = "\n".join(comment_list)
|
||||||
email_body = templater.get_template("new_comment").render(url=comment.url, comment=comment_text)
|
email_body = get_template("new_comment").render(
|
||||||
|
url=comment.url, comment=comment_text
|
||||||
|
)
|
||||||
|
|
||||||
site = Site.select().where(Site.id == Comment.site).get()
|
site = Site.select().where(Site.id == Comment.site).get()
|
||||||
# send email
|
# send email
|
||||||
subject = "STACOSYS %s: [%d:%s]" % (site.name, comment.id, site.token)
|
subject = "STACOSYS %s: [%d:%s]" % (site.name, comment.id, site.token)
|
||||||
mailer.send_mail(site.admin_email, subject, email_body)
|
mailer.send(site.admin_email, subject, email_body)
|
||||||
logger.debug("new comment processed ")
|
logger.debug("new comment processed ")
|
||||||
|
|
||||||
|
|
||||||
|
def reply_comment_email(data):
|
||||||
|
|
||||||
|
from_email = data["from"]
|
||||||
|
subject = data["subject"]
|
||||||
|
message = ""
|
||||||
|
for part in data["parts"]:
|
||||||
|
if part["content-type"] == "text/plain":
|
||||||
|
message = part["content"]
|
||||||
|
break
|
||||||
|
|
||||||
|
m = re.search(r"\[(\d+)\:(\w+)\]", subject)
|
||||||
|
if not m:
|
||||||
|
logger.warn("ignore corrupted email. No token %s" % subject)
|
||||||
|
return
|
||||||
|
comment_id = int(m.group(1))
|
||||||
|
token = m.group(2)
|
||||||
|
|
||||||
|
# retrieve site and comment rows
|
||||||
|
try:
|
||||||
|
comment = Comment.select().where(Comment.id == comment_id).get()
|
||||||
|
except:
|
||||||
|
logger.warn("unknown comment %d" % comment_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if comment.published:
|
||||||
|
logger.warn("ignore already published email. token %d" % comment_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if comment.site.token != token:
|
||||||
|
logger.warn("ignore corrupted email. Unknown token %d" % comment_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
logger.warn("ignore empty email")
|
||||||
|
return
|
||||||
|
|
||||||
|
# safe logic: no answer or unknown answer is a go for publishing
|
||||||
|
if message[:2].upper() in ("NO", "SP"):
|
||||||
|
|
||||||
|
# put a log to help fail2ban
|
||||||
|
if message[:2].upper() == "SP": # SPAM
|
||||||
|
if comment_id in client_ips:
|
||||||
|
logger.info(
|
||||||
|
"SPAM comment from %s: %d" % (client_ips[comment_id], comment_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("cannot identify SPAM source: %d" % comment_id)
|
||||||
|
|
||||||
|
# forget client IP
|
||||||
|
if comment_id in client_ips:
|
||||||
|
del client_ips[comment_id]
|
||||||
|
|
||||||
|
logger.info("discard comment: %d" % comment_id)
|
||||||
|
comment.delete_instance()
|
||||||
|
email_body = get_template("drop_comment").render(original=message)
|
||||||
|
mailer.send(from_email, "Re: " + subject, email_body)
|
||||||
|
else:
|
||||||
|
# update Comment row
|
||||||
|
comment.published = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
comment.save()
|
||||||
|
logger.info("commit comment: %d" % comment_id)
|
||||||
|
|
||||||
|
# rebuild RSS
|
||||||
|
rss.generate_site(token)
|
||||||
|
|
||||||
|
# send approval confirmation email to admin
|
||||||
|
email_body = get_template("approve_comment").render(original=message)
|
||||||
|
mailer.send(from_email, "Re: " + subject, email_body)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
def fetch():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def send(email, subject, body):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete(content):
|
||||||
|
# TODO delete mail
|
||||||
|
pass
|
||||||
|
|
|
@ -12,7 +12,7 @@ from threading import Thread
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from model.site import Site
|
from model.site import Site
|
||||||
from model.comment import Comment
|
from model.comment import Comment
|
||||||
from helpers.hashing import md5
|
from helper.hashing import md5
|
||||||
from conf import config
|
from conf import config
|
||||||
from core import mailer
|
from core import mailer
|
||||||
|
|
||||||
|
@ -26,95 +26,7 @@ env = None
|
||||||
client_ips = {}
|
client_ips = {}
|
||||||
|
|
||||||
|
|
||||||
class Processor(Thread):
|
|
||||||
def stop(self):
|
|
||||||
logger.info("stop requested")
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
|
|
||||||
logger.info("processor thread started")
|
|
||||||
self.is_running = True
|
|
||||||
while self.is_running:
|
|
||||||
try:
|
|
||||||
msg = queue.get()
|
|
||||||
if msg["request"] == "new_mail":
|
|
||||||
reply_comment_email(msg["data"])
|
|
||||||
send_delete_command(msg["data"])
|
|
||||||
else:
|
|
||||||
logger.info("throw unknown request " + str(msg))
|
|
||||||
except:
|
|
||||||
logger.exception("processing failure")
|
|
||||||
|
|
||||||
|
|
||||||
def reply_comment_email(data):
|
|
||||||
|
|
||||||
from_email = data["from"]
|
|
||||||
subject = data["subject"]
|
|
||||||
message = ""
|
|
||||||
for part in data["parts"]:
|
|
||||||
if part["content-type"] == "text/plain":
|
|
||||||
message = part["content"]
|
|
||||||
break
|
|
||||||
|
|
||||||
m = re.search("\[(\d+)\:(\w+)\]", subject)
|
|
||||||
if not m:
|
|
||||||
logger.warn("ignore corrupted email. No token %s" % subject)
|
|
||||||
return
|
|
||||||
comment_id = int(m.group(1))
|
|
||||||
token = m.group(2)
|
|
||||||
|
|
||||||
# retrieve site and comment rows
|
|
||||||
try:
|
|
||||||
comment = Comment.select().where(Comment.id == comment_id).get()
|
|
||||||
except:
|
|
||||||
logger.warn("unknown comment %d" % comment_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
if comment.published:
|
|
||||||
logger.warn("ignore already published email. token %d" % comment_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
if comment.site.token != token:
|
|
||||||
logger.warn("ignore corrupted email. Unknown token %d" % comment_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not message:
|
|
||||||
logger.warn("ignore empty email")
|
|
||||||
return
|
|
||||||
|
|
||||||
# safe logic: no answer or unknown answer is a go for publishing
|
|
||||||
if message[:2].upper() in ("NO", "SP"):
|
|
||||||
|
|
||||||
# put a log to help fail2ban
|
|
||||||
if message[:2].upper() == "SP": # SPAM
|
|
||||||
if comment_id in client_ips:
|
|
||||||
logger.info(
|
|
||||||
"SPAM comment from %s: %d" % (client_ips[comment_id], comment_id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info("cannot identify SPAM source: %d" % comment_id)
|
|
||||||
|
|
||||||
# forget client IP
|
|
||||||
if comment_id in client_ips:
|
|
||||||
del client_ips[comment_id]
|
|
||||||
|
|
||||||
logger.info("discard comment: %d" % comment_id)
|
|
||||||
comment.delete_instance()
|
|
||||||
email_body = get_template("drop_comment").render(original=message)
|
|
||||||
mail(from_email, "Re: " + subject, email_body)
|
|
||||||
else:
|
|
||||||
# update Comment row
|
|
||||||
comment.published = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
comment.save()
|
|
||||||
logger.info("commit comment: %d" % comment_id)
|
|
||||||
|
|
||||||
# rebuild RSS
|
|
||||||
rss(token)
|
|
||||||
|
|
||||||
# send approval confirmation email to admin
|
|
||||||
email_body = get_template("approve_comment").render(original=message)
|
|
||||||
mail(from_email, "Re: " + subject, email_body)
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_metadata(message):
|
def get_email_metadata(message):
|
||||||
|
@ -126,73 +38,3 @@ def get_email_metadata(message):
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
|
||||||
def rss(token, onstart=False):
|
|
||||||
|
|
||||||
if onstart and os.path.isfile(config.rss["file"]):
|
|
||||||
return
|
|
||||||
|
|
||||||
site = Site.select().where(Site.token == token).get()
|
|
||||||
rss_title = get_template("rss_title_message").render(site=site.name)
|
|
||||||
md = markdown.Markdown()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for row in (
|
|
||||||
Comment.select()
|
|
||||||
.join(Site)
|
|
||||||
.where(Site.token == token, Comment.published)
|
|
||||||
.order_by(-Comment.published)
|
|
||||||
.limit(10)
|
|
||||||
):
|
|
||||||
item_link = "%s://%s%s" % (config.rss["proto"], site.url, row.url)
|
|
||||||
items.append(
|
|
||||||
PyRSS2Gen.RSSItem(
|
|
||||||
title="%s - %s://%s%s"
|
|
||||||
% (config.rss["proto"], row.author_name, 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" % (config.rss["proto"], site.url),
|
|
||||||
description="Commentaires du site '%s'" % site.name,
|
|
||||||
lastBuildDate=datetime.now(),
|
|
||||||
items=items,
|
|
||||||
)
|
|
||||||
rss.write_xml(open(config.rss["file"], "w"), encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def send_delete_command(content):
|
|
||||||
# TODO delete mail
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_template(name):
|
|
||||||
return env.get_template(config.general["lang"] + "/" + name + ".tpl")
|
|
||||||
|
|
||||||
|
|
||||||
def enqueue(something):
|
|
||||||
queue.put(something)
|
|
||||||
|
|
||||||
|
|
||||||
def get_processor():
|
|
||||||
return proc
|
|
||||||
|
|
||||||
|
|
||||||
def start(template_dir):
|
|
||||||
global proc, env
|
|
||||||
|
|
||||||
# initialize Jinja 2 templating
|
|
||||||
logger.info("load templates from directory %s" % template_dir)
|
|
||||||
env = Environment(loader=FileSystemLoader(template_dir))
|
|
||||||
|
|
||||||
# generate RSS for all sites
|
|
||||||
for site in Site.select():
|
|
||||||
rss(site.token, True)
|
|
||||||
|
|
||||||
# start processor thread
|
|
||||||
proc = Processor()
|
|
||||||
proc.start()
|
|
||||||
|
|
52
app/core/rss.py
Normal file
52
app/core/rss.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import markdown
|
||||||
|
import PyRSS2Gen
|
||||||
|
from model.site import Site
|
||||||
|
from model.comment import Comment
|
||||||
|
from core.templater import get_template
|
||||||
|
from conf import config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_all():
|
||||||
|
for site in Site.select():
|
||||||
|
generate_site(site.token)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_site(token):
|
||||||
|
|
||||||
|
site = Site.select().where(Site.token == token).get()
|
||||||
|
rss_title = get_template("rss_title_message").render(site=site.name)
|
||||||
|
md = markdown.Markdown()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for row in (
|
||||||
|
Comment.select()
|
||||||
|
.join(Site)
|
||||||
|
.where(Site.token == token, Comment.published)
|
||||||
|
.order_by(-Comment.published)
|
||||||
|
.limit(10)
|
||||||
|
):
|
||||||
|
item_link = "%s://%s%s" % (config.get(config.RSS_PROTO), site.url, row.url)
|
||||||
|
items.append(
|
||||||
|
PyRSS2Gen.RSSItem(
|
||||||
|
title="%s - %s://%s%s"
|
||||||
|
% (config.get(config.RSS_PROTO), row.author_name, 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" % (config.get(config.RSS_PROTO), site.url),
|
||||||
|
description="Commentaires du site '%s'" % site.name,
|
||||||
|
lastBuildDate=datetime.now(),
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
rss.write_xml(open(config.get(config.RSS_FILE), "w"), encoding="utf-8")
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
@ -13,4 +12,4 @@ env = Environment(loader=FileSystemLoader(template_path))
|
||||||
|
|
||||||
|
|
||||||
def get_template(name):
|
def get_template(name):
|
||||||
return env.get_template(config.general["lang"] + "/" + name + ".tpl")
|
return env.get_template(config.get(config.LANG) + "/" + name + ".tpl")
|
||||||
|
|
|
@ -7,7 +7,7 @@ from flask import request, abort, redirect
|
||||||
from model.site import Site
|
from model.site import Site
|
||||||
from model.comment import Comment
|
from model.comment import Comment
|
||||||
from conf import config
|
from conf import config
|
||||||
from helpers.hashing import md5
|
from helper.hashing import md5
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
app = config.flaskapp()
|
app = config.flaskapp()
|
||||||
|
|
11
app/run.py
11
app/run.py
|
@ -59,9 +59,6 @@ def stacosys_server(config_pathname):
|
||||||
|
|
||||||
database.setup()
|
database.setup()
|
||||||
|
|
||||||
# start processor
|
|
||||||
from core import processor
|
|
||||||
|
|
||||||
# cron email fetcher
|
# cron email fetcher
|
||||||
app.config.from_object(
|
app.config.from_object(
|
||||||
JobConfig(
|
JobConfig(
|
||||||
|
@ -74,10 +71,18 @@ def stacosys_server(config_pathname):
|
||||||
|
|
||||||
logger.info("Start Stacosys application")
|
logger.info("Start Stacosys application")
|
||||||
|
|
||||||
|
# generate RSS for all sites
|
||||||
|
from core import rss
|
||||||
|
|
||||||
|
rss.generate_all()
|
||||||
|
|
||||||
# start Flask
|
# start Flask
|
||||||
from interface import api
|
from interface import api
|
||||||
from interface import form
|
from interface import form
|
||||||
|
|
||||||
|
logger.debug("Load interface %s" % api)
|
||||||
|
logger.debug("Load interface %s" % form)
|
||||||
|
|
||||||
app.run(
|
app.run(
|
||||||
host=config.get(config.HTTP_HOST),
|
host=config.get(config.HTTP_HOST),
|
||||||
port=config.get(config.HTTP_PORT),
|
port=config.get(config.HTTP_PORT),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
[main]
|
[main]
|
||||||
lang = fr
|
lang = fr
|
||||||
db_url = sqlite:///db.sqlite
|
db_url = sqlite:///db.sqlite
|
||||||
|
newcomment_polling = 60
|
||||||
|
|
||||||
[http]
|
[http]
|
||||||
root_url = http://localhost:8100
|
root_url = http://localhost:8100
|
||||||
|
@ -16,6 +17,6 @@ secret = Uqca5Kc8xuU6THz9
|
||||||
proto = http
|
proto = http
|
||||||
file = comments.xml
|
file = comments.xml
|
||||||
|
|
||||||
[polling]
|
[mail]
|
||||||
newmail = 15
|
fetch_polling = 15
|
||||||
newcomment = 60
|
mailer_url = http://localhost:8000
|
||||||
|
|
Loading…
Add table
Reference in a new issue