First commit for web control

This commit is contained in:
louis chauvet 2018-10-08 23:41:54 +02:00
parent 33b139bb34
commit 13709a2059
24 changed files with 840 additions and 207 deletions

View File

@ -8,6 +8,9 @@ name = "pypi"
"discord.py" = {ref = "rewrite", git = "https://github.com/Rapptz/discord.py"} "discord.py" = {ref = "rewrite", git = "https://github.com/Rapptz/discord.py"}
mysql-connector-python = "*" mysql-connector-python = "*"
pymysql = "*" pymysql = "*"
tornado = "*"
bcrypt = "*"
markdown = "*"
[dev-packages] [dev-packages]

70
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0a45806745c14c2eb4a5190e94d4093508347d44f685b6b7262259c46b5f7cb5" "sha256": "12ccc168a0520cd43d8a2ca05e3cbe21dd5262fd5a48e4eab57d368f713cb0c7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -23,6 +23,46 @@
], ],
"version": "==0.24.0" "version": "==0.24.0"
}, },
"bcrypt": {
"hashes": [
"sha256:01477981abf74e306e8ee31629a940a5e9138de000c6b0898f7f850461c4a0a5",
"sha256:054d6e0acaea429e6da3613fcd12d05ee29a531794d96f6ab959f29a39f33391",
"sha256:0872eeecdf9a429c1420158500eedb323a132bc5bf3339475151c52414729e70",
"sha256:09a3b8c258b815eadb611bad04ca15ec77d86aa9ce56070e1af0d5932f17642a",
"sha256:0f317e4ffbdd15c3c0f8ab5fbd86aa9aabc7bea18b5cc5951b456fe39e9f738c",
"sha256:2788c32673a2ad0062bea850ab73cffc0dba874db10d7a3682b6f2f280553f20",
"sha256:321d4d48be25b8d77594d8324c0585c80ae91ac214f62db9098734e5e7fb280f",
"sha256:346d6f84ff0b493dbc90c6b77136df83e81f903f0b95525ee80e5e6d5e4eef84",
"sha256:34dd60b90b0f6de94a89e71fcd19913a30e83091c8468d0923a93a0cccbfbbff",
"sha256:3b4c23300c4eded8895442c003ae9b14328ae69309ac5867e7530de8bdd7875d",
"sha256:43d1960e7db14042319c46925892d5fa99b08ff21d57482e6f5328a1aca03588",
"sha256:49e96267cd9be55a349fd74f9852eb9ae2c427cd7f6455d0f1765d7332292832",
"sha256:63e06ffdaf4054a89757a3a1ab07f1b922daf911743114a54f7c561b9e1baa58",
"sha256:67ed1a374c9155ec0840214ce804616de49c3df9c5bc66740687c1c9b1cd9e8d",
"sha256:6b662a5669186439f4f583636c8d6ea77cf92f7cfe6aae8d22edf16c36840574",
"sha256:6efd9ca20aefbaf2e7e6817a2c6ed4a50ff6900fafdea1bcb1d0e9471743b144",
"sha256:8569844a5d8e1fdde4d7712a05ab2e6061343ac34af6e7e3d7935b2bd1907bfd",
"sha256:8629ea6a8a59f865add1d6a87464c3c676e60101b8d16ef404d0a031424a8491",
"sha256:988cac675e25133d01a78f2286189c1f01974470817a33eaf4cfee573cfb72a5",
"sha256:9a6fedda73aba1568962f7543a1f586051c54febbc74e87769bad6a4b8587c39",
"sha256:9eced8962ce3b7124fe20fd358cf8c7470706437fa064b9874f849ad4c5866fc",
"sha256:a005ed6163490988711ff732386b08effcbf8df62ae93dd1e5bda0714fad8afb",
"sha256:ae35dbcb6b011af6c840893b32399252d81ff57d52c13e12422e16b5fea1d0fb",
"sha256:b1e8491c6740f21b37cca77bc64677696a3fb9f32360794d57fa8477b7329eda",
"sha256:c906bdb482162e9ef48eea9f8c0d967acceb5c84f2d25574c7d2a58d04861df1",
"sha256:cb18ffdc861dbb244f14be32c47ab69604d0aca415bee53485fcea4f8e93d5ef",
"sha256:cc2f24dc1c6c88c56248e93f28d439ee4018338567b0bbb490ea26a381a29b1e",
"sha256:d860c7fff18d49e20339fc6dffc2d485635e36d4b2cccf58f45db815b64100b4",
"sha256:d86da365dda59010ba0d1ac45aa78390f56bf7f992e65f70b3b081d5e5257b09",
"sha256:e22f0997622e1ceec834fd25947dc2ee2962c2133ea693d61805bc867abaf7ea",
"sha256:f2fe545d27a619a552396533cddf70d83cecd880a611cdfdbb87ca6aec52f66b",
"sha256:f425e925485b3be48051f913dbe17e08e8c48588fdf44a26b8b14067041c0da6",
"sha256:f7fd3ed3745fe6e81e28dc3b3d76cce31525a91f32a387e1febd6b982caf8cdb",
"sha256:f9210820ee4818d84658ed7df16a7f30c9fba7d8b139959950acef91745cc0f7"
],
"index": "pypi",
"version": "==3.1.4"
},
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
@ -86,7 +126,7 @@
}, },
"discord.py": { "discord.py": {
"git": "https://github.com/Rapptz/discord.py", "git": "https://github.com/Rapptz/discord.py",
"ref": "00a659c6526b2445162b52eaf970adbd22c6d35d" "ref": "418048b98abef627f57f9e28e268bf3a8668648a"
}, },
"fs.dropboxfs": { "fs.dropboxfs": {
"git": "https://github.com/rkhwaja/fs.dropboxfs.git", "git": "https://github.com/rkhwaja/fs.dropboxfs.git",
@ -99,6 +139,14 @@
], ],
"version": "==2.7" "version": "==2.7"
}, },
"markdown": {
"hashes": [
"sha256:80f44d67c4f34db6ae8210a7194c7335923744181b6240e06d67479478eb7bb9",
"sha256:b853a125f03db3f2fdbcbc96fb738f2a7f2cdabc3f1262a4d89121c6ce1bd7e3"
],
"index": "pypi",
"version": "==3.0"
},
"mysql-connector-python": { "mysql-connector-python": {
"hashes": [ "hashes": [
"sha256:35a8f77b90d40cbf5bbb87bcfae02d63ca0383833187142ead963b1ad95ee958", "sha256:35a8f77b90d40cbf5bbb87bcfae02d63ca0383833187142ead963b1ad95ee958",
@ -142,9 +190,10 @@
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
], ],
"version": "==2.18" "markers": "python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*'",
"version": "==2.19"
}, },
"pymysql": { "pymysql": {
"hashes": [ "hashes": [
@ -160,6 +209,19 @@
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
], ],
"version": "==1.11.0" "version": "==1.11.0"
},
"tornado": {
"hashes": [
"sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d",
"sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409",
"sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f",
"sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f",
"sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5",
"sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb",
"sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444"
],
"index": "pypi",
"version": "==5.1.1"
} }
}, },
"develop": {} "develop": {}

152
bot/fobot.py Normal file
View File

@ -0,0 +1,152 @@
import importlib
import json
import os
import re
import logging
import discord
log_discord = logging.getLogger('discord')
log_foBot = logging.getLogger('foBot')
debug = log_foBot.debug
info = log_foBot.info
warning = log_foBot.warning
error = log_foBot.error
critical = log_foBot.critical
def to_str(entier):
return str(entier).replace("1", "a").replace("2", "b").replace("3", "c").replace("4", "d").replace("5", "e") \
.replace("6", "f").replace("7", "g").replace("8", "h").replace("9", "i").replace("0", "j")
class Guild:
def __init__(self, bot, guild_id):
self.id = guild_id
self.bot = bot
self.config = {"modules": ["modules"],
"prefix": "§",
"master_admins": [318866596502306816],
"lang": "FR_fr"
}
self.modules = []
self.load_config()
self.update_modules()
self.save_config()
def load_config(self):
with self.bot.database.cursor() as cursor:
# Create guild table if it not exists
sql_create = """CREATE TABLE IF NOT EXISTS {guild_id} (
id int(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,
name varchar(50) NOT NULL,
content JSON CHECK (JSON_VALID(content))
);""".format(guild_id=to_str(self.id))
cursor.execute(sql_create)
# Load config row
sql_content = """SELECT id,name,content FROM {guild_id} WHERE name='config';""".format(
guild_id=to_str(self.id))
cursor.execute(sql_content)
result = cursor.fetchone()
if result is None:
sql_insert = """INSERT INTO {guild_id} (name) VALUES ('config');""".format(guild_id=to_str(self.id))
cursor.execute(sql_insert)
self.save_config()
# Refetch config
sql_content = """SELECT id,name,content FROM {guild_id} WHERE name='config';""".format(
guild_id=to_str(self.id))
cursor.execute(sql_content)
result = cursor.fetchone()
self.config = json.loads(result['content'])
self.bot.database.commit()
def save_config(self):
with self.bot.database.cursor() as cursor:
sql = r"""UPDATE {guild_id} SET content='{configjson}' WHERE name='config';""".format(
guild_id=to_str(self.id),
configjson=re.escape(json.dumps(self.config)))
cursor.execute(sql)
self.bot.database.commit()
def update_modules(self):
self.modules = []
errors = []
if "modules" not in self.config["modules"]:
self.config["modules"].append("modules")
if "help" not in self.config["modules"]:
self.config["modules"].append("help")
module_to_load = list(set(self.config["modules"]))
self.config["modules"] = module_to_load
self.save_config()
for module in module_to_load:
# Try to load all modules by name
if module not in self.bot.modules.keys():
# Module is not an existing module
self.config["modules"].remove(module)
# Write an error in log
error("Module %s doesn't exists." % module)
errors.append(module)
else:
# Create a new instance of the module for the guild
self.modules.append(self.bot.modules[module](guild=self))
return errors
async def on_message(self, msg):
if not msg.author.bot:
for module in self.modules:
await module.on_message(msg)
print(msg.author, msg.content)
return
class FoBot(discord.Client):
def __init__(self, config='/foBot_config', db_connection=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.config_folder = config
self.config = {"guilds": {}}
self.guilds_class = {}
self.modules = {}
self.load_modules()
self.database = db_connection
def load_modules(self):
for module in os.listdir(os.path.join("bot", 'modules')):
if module != "__pycache__" and module.endswith(".py"):
imported = importlib.import_module('bot.modules.' + module[:-3])
self.modules.update({module[:-3]: imported.MainClass})
def load_config(self):
for guild in self.guilds:
self.guilds_class.update({guild.id: Guild(self, guild.id)})
def save_config(self):
pass
async def on_connect(self):
info("foBot is connected.")
async def on_ready(self):
info("foBot is ready to listen discord.")
info("Load foBot configuration.")
self.load_config()
self.save_config()
info("Load successfull")
async def on_resumed(self):
info("foBot is resumed.")
async def on_guild_join(self, guild):
self.load_modules()
self.load_config()
self.save_config()
async def on_error(self, event, *args, **kwargs):
error("foBot encounter an error.", exc_info=True)
async def on_message(self, msg):
await self.guilds_class[msg.guild.id].on_message(msg)

View File

@ -1,5 +1,5 @@
import discord import discord
import traductions as tr from bot import traductions as tr
class MainClass: class MainClass:

View File

@ -1,5 +1,4 @@
import discord from bot import traductions as tr
import traductions as tr
class MainClass: class MainClass:

View File

@ -1,5 +1,4 @@
import discord from bot import traductions as tr
import traductions as tr
class MainClass: class MainClass:

View File

@ -1,5 +1,5 @@
import discord import discord
import traductions as tr from bot import traductions as tr
class MainClass: class MainClass:

View File

@ -1,9 +1,6 @@
import os
import re import re
import fs from bot import traductions as tr
import traductions as tr
class MainClass: class MainClass:

View File

@ -1,7 +1,7 @@
import time import time
import discord import discord
import traductions as tr from bot import traductions as tr
class MainClass: class MainClass:

View File

@ -1,59 +1,74 @@
{ {
"version": 1, "version": 1,
"disable_existing_loggers": false, "disable_existing_loggers": false,
"formatters": { "formatters": {
"simple": { "simple": {
"format": "%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s" "format": "%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": "info.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"error_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "simple",
"filename": "errors.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"sms_handler": {
"class":"SMSHandler.SMSHandler",
"level":"ERROR",
"formatter": "simple"
}
},
"loggers": {
"foBot": {
"level": "DEBUG",
"handlers": ["console", "info_file_handler", "error_file_handler", "sms_handler"]
},
"discord": {
"level":"WARNING",
"handlers":["console","error_file_handler"]
}
},
"root": {
"level": "INFO",
"handlers": ["console", "info_file_handler", "error_file_handler"]
} }
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": "info.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"error_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "simple",
"filename": "errors.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"sms_handler": {
"class": "SMSHandler.SMSHandler",
"level": "ERROR",
"formatter": "simple"
}
},
"loggers": {
"foBot": {
"level": "DEBUG",
"handlers": [
"console",
"info_file_handler",
"error_file_handler",
"sms_handler"
]
},
"discord": {
"level": "WARNING",
"handlers": [
"console",
"error_file_handler"
]
},
"webserver": {
"level": "DEBUG",
"handlers": [
"console",
"info_file_handler",
"error_file_handler"
]
}
},
"root": {
"level": "INFO",
"handlers": [
"console",
"info_file_handler",
"error_file_handler"
]
}
} }

151
main.py
View File

@ -1,9 +1,13 @@
import importlib import asyncio
import json import json
import logging import logging
import logging.config import logging.config
import re import tornado.ioloop
import discord import tornado.web
from bot.fobot import FoBot
from web.server import FoWeb
import pymysql as mariadb import pymysql as mariadb
@ -61,146 +65,21 @@ def setup_logging(default_path='log_config.json', default_level=logging.INFO, en
setup_logging() setup_logging()
log_discord = logging.getLogger('discord')
log_foBot = logging.getLogger('foBot')
debug = log_foBot.debug
info = log_foBot.info
warning = log_foBot.warning
error = log_foBot.error
critical = log_foBot.critical
class Guild:
def __init__(self, bot, guild_id):
self.id = guild_id
self.bot = bot
self.config = {"modules": ["modules"],
"prefix": "§",
"master_admins": [318866596502306816],
"lang": "FR_fr"
}
self.modules = []
self.load_config()
self.update_modules()
self.save_config()
def load_config(self):
with self.bot.database.cursor() as cursor:
# Create guild table if it not exists
sql_create = """CREATE TABLE IF NOT EXISTS {guild_id} (
id int(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,
name varchar(50) NOT NULL,
content JSON CHECK (JSON_VALID(content))
);""".format(guild_id=to_str(self.id))
cursor.execute(sql_create)
# Load config row
sql_content = """SELECT id,name,content FROM {guild_id} WHERE name='config';""".format(
guild_id=to_str(self.id))
cursor.execute(sql_content)
result = cursor.fetchone()
if result is None:
sql_insert = """INSERT INTO {guild_id} (name) VALUES ('config');""".format(guild_id=to_str(self.id))
cursor.execute(sql_insert)
self.save_config()
# Refetch config
sql_content = """SELECT id,name,content FROM {guild_id} WHERE name='config';""".format(
guild_id=to_str(self.id))
cursor.execute(sql_content)
result = cursor.fetchone()
self.config = json.loads(result['content'])
self.bot.database.commit()
def save_config(self):
with self.bot.database.cursor() as cursor:
sql = r"""UPDATE {guild_id} SET content='{configjson}' WHERE name='config';""".format(
guild_id=to_str(self.id),
configjson=re.escape(json.dumps(self.config)))
cursor.execute(sql)
self.bot.database.commit()
def update_modules(self):
self.modules = []
errors = []
if "modules" not in self.config["modules"]:
self.config["modules"].append("modules")
if "help" not in self.config["modules"]:
self.config["modules"].append("help")
module_to_load = list(set(self.config["modules"]))
self.config["modules"] = module_to_load
self.save_config()
for module in module_to_load:
# Try to load all modules by name
if module not in self.bot.modules.keys():
# Module is not an existing module
self.config["modules"].remove(module)
# Write an error in log
error("Module %s doesn't exists." % module)
errors.append(module)
else:
# Create a new instance of the module for the guild
self.modules.append(self.bot.modules[module](guild=self))
return errors
async def on_message(self, msg):
if not msg.author.bot:
for module in self.modules:
await module.on_message(msg)
print(msg.content)
return
class FoBot(discord.Client):
def __init__(self, config='/foBot_config', *args, **kwargs): eventloop = asyncio.get_event_loop()
super().__init__(*args, **kwargs)
self.config_folder = config
self.config = {"guilds": {}}
self.guilds_class = {}
self.modules = {}
self.load_modules()
self.database = db_connection
def load_modules(self): foBot = FoBot(db_connection=db_connection)
for module in os.listdir('modules'):
if module != "__pycache__" and module.endswith(".py"):
imported = importlib.import_module('modules.' + module[:-3])
self.modules.update({module[:-3]: imported.MainClass})
def load_config(self): foWeb = FoWeb(bot=None, db=db_connection)
for guild in self.guilds:
self.guilds_class.update({guild.id: Guild(self, guild.id)})
def save_config(self): bot_app = foBot.start(os.environ['DISCORD_TOKEN'], max_messages=100000000)
pass bot_task = asyncio.ensure_future(bot_app)
async def on_connect(self): foWeb.listen(port=8888)
info("foBot is connected.") web_task = foWeb.get_task()
async def on_ready(self): eventloop.run_forever()
info("foBot is ready to listen discord.")
info("Load foBot configuration.")
self.load_config()
self.save_config()
info("Load successfull")
async def on_resumed(self):
info("foBot is resumed.")
async def on_guild_join(self, guild):
self.load_modules()
self.load_config()
self.save_config()
async def on_error(self, event, *args, **kwargs):
error("foBot encounter an error.", exc_info=True)
async def on_message(self, msg):
await self.guilds_class[msg.guild.id].on_message(msg)
myBot = FoBot()
myBot.run(os.environ['DISCORD_TOKEN'], max_messages=100000000)

249
web/server.py Normal file
View File

@ -0,0 +1,249 @@
import asyncio
import os
import re
import unicodedata
import bcrypt as bcrypt
import markdown
import tornado
import tornado.web
def maybe_create_tables(db):
with db.cursor() as cur:
#cur.execute("DROP TABLE users ")
cur.execute("CREATE TABLE IF NOT EXISTS users ("
" id int(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,"
" email VARCHAR(100) NOT NULL UNIQUE,"
" name VARCHAR(100) NOT NULL,"
" hashed_password VARCHAR(100) NOT NULL,"
" author int(5) NOT NULL DEFAULT 0"
")")
cur.execute("CREATE TABLE IF NOT EXISTS entries ("
" id int(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,"
" author_id INT(5) NOT NULL REFERENCES authors(id),"
" slug VARCHAR(100) NOT NULL UNIQUE,"
" title VARCHAR(512) NOT NULL,"
" markdown TEXT NOT NULL,"
" html TEXT NOT NULL,"
" published TIMESTAMP NOT NULL,"
" updated TIMESTAMP NOT NULL"
")")
db.commit()
class NoResultError(BaseException):
pass
class BaseHandler(tornado.web.RequestHandler):
def row_to_obj(self, row, cur):
"""Convert a SQL row to an object supporting dict and attribute access."""
obj = tornado.util.ObjectDict()
for val, desc in zip(row, cur.description):
obj[desc[0]] = row[desc[0]]
return obj
def execute(self, stmt, *args):
"""Execute a SQL statement.
Must be called with ``await self.execute(...)``
"""
with self.application.db.cursor() as cur:
cur.execute(stmt, args)
self.application.db.commit()
def query(self, stmt, *args):
"""Query for a list of results.
Typical usage::
results = await self.query(...)
Or::
for row in await self.query(...)
"""
with self.application.db.cursor() as cur:
cur.execute(stmt, args)
return [self.row_to_obj(row, cur) for row in cur.fetchall()]
def queryone(self, stmt, *args):
"""Query for exactly one result.
Raises NoResultError if there are no results, or ValueError if
there are more than one.
"""
results = self.query(stmt, *args)
if len(results) == 0:
raise NoResultError()
elif len(results) > 1:
raise ValueError("Expected 1 result, got %d" % len(results))
return results[0]
def prepare(self):
# get_current_user cannot be a coroutine, so set
# self.current_user in prepare instead.
user_id = self.get_secure_cookie("blogdemo_user")
if user_id:
self.current_user = self.queryone("SELECT * FROM users WHERE id = %s",
int(user_id))
def any_users_exists(self):
return bool(self.query("SELECT * FROM users LIMIT 1"))
class BlogComposeHandler(BaseHandler):
@tornado.web.authenticated
async def get(self):
user = self.current_user
idarticle = self.get_argument("id", None)
entry = None
if idarticle:
entry = self.queryone("SELECT * FROM entries WHERE id = %s", int(idarticle))
self.render("blog\\compose.html", entry=entry, user=user)
@tornado.web.authenticated
async def post(self):
id = self.get_argument("id", None)
title = self.get_argument("title")
text = self.get_argument("markdown")
html = markdown.markdown(text)
if id:
try:
entry = self.queryone("SELECT * FROM entries WHERE id = %s", int(id))
except NoResultError:
raise tornado.web.HTTPError(404)
slug = entry.slug
self.execute(
"UPDATE entries SET title = %s, markdown = %s, html = %s "
"WHERE id = %s", title, text, html, int(id))
else:
slug = unicodedata.normalize("NFKD", title)
slug = re.sub(r"[^\w]+", " ", slug)
slug = "-".join(slug.lower().strip().split())
slug = slug.encode("ascii", "ignore").decode("ascii")
if not slug:
slug = "entry"
while True:
e = self.query("SELECT * FROM entries WHERE slug = %s", slug)
if not e:
break
slug += "-2"
self.execute(
"INSERT INTO entries (author_id,title,slug,markdown,html,published,updated)"
"VALUES (%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)",
self.current_user.id, title, slug, text, html)
self.redirect("/blog/entry/" + slug)
class IndexHandler(BaseHandler):
async def get(self):
self.render("index.html")
class AuthLoginHandler(BaseHandler):
async def get(self):
get_arg = self.get_argument
self.render("auth/login.html", error=None, get_arg=get_arg)
async def post(self):
try:
user = self.queryone("SELECT * FROM users WHERE email = %s",
self.get_argument("email"))
print(user)
except NoResultError:
get_arg = self.get_argument
self.render("auth/login.html", error="Email not found or bad password", get_arg=get_arg)
return
verified = await tornado.ioloop.IOLoop.current().run_in_executor(
None, bcrypt.checkpw, tornado.escape.utf8(self.get_argument("password")),
user.hashed_password.encode("utf-8"))
if verified:
self.set_secure_cookie("blogdemo_user", str(user.id))
self.redirect(self.get_argument("next", "/"))
else:
get_arg = self.get_argument
self.render("auth/login.html", error="Email not found or bad password", get_arg=get_arg)
class AuthLogoutHandler(BaseHandler):
def get(self):
self.clear_cookie("blogdemo_user")
self.redirect(self.get_argument("next", "/"))
class AuthCreateHandler(BaseHandler):
async def get(self):
get_arg = self.get_argument
self.render("auth/create_user.html", get_arg=get_arg)
async def post(self):
hashed_password = await tornado.ioloop.IOLoop.current().run_in_executor(
None, bcrypt.hashpw, tornado.escape.utf8(self.get_argument("password")),
bcrypt.gensalt()
)
self.execute("INSERT INTO users (email, name, hashed_password) VALUES (%s, %s, %s)",
self.get_argument("email"), self.get_argument("name"),
tornado.escape.to_unicode(hashed_password))
users = self.queryone("SELECT * FROM users WHERE email = %s", self.get_argument("email"))
print(users)
self.set_secure_cookie("blogdemo_user", str(users.id))
self.redirect(self.get_argument("next", "/"))
class BlogHomeHandler(BaseHandler):
async def get(self):
entries = self.query("SELECT * FROM entries ORDER BY published DESC LIMIT 5")
if not entries:
self.redirect("/blog/compose")
return
self.render("blog\\index.html", entries=entries)
class BlogPostHandler(BaseHandler):
async def get(self, slug):
entry = self.queryone("SELECT * FROM entries WHERE slug = %s", slug)
if not entry:
raise tornado.web.HTTPError(404)
self.render("blog\\entry.html", entry=entry)
class BotConnectHandler(BaseHandler):
pass
class BotConfigureHandler(BaseHandler):
pass
class BlogEntryModule(tornado.web.UIModule):
def render(self, entry):
return self.render_string("blog\\modules\\entry.html", entry=entry)
class FoWeb(tornado.web.Application):
def __init__(self, bot, db):
self.db = db
maybe_create_tables(self.db)
handlers = [
(r"/", IndexHandler),
(r"/blog/compose", BlogComposeHandler),
(r"/auth/login", AuthLoginHandler),
(r"/auth/logout", AuthLogoutHandler),
(r"/auth/create", AuthCreateHandler),
(r"/blog", BlogHomeHandler),
(r"/blog/entry/([^/]+)", BlogPostHandler),
(r"/bot/connect", BotConnectHandler),
(r"/bot/configure", BotConfigureHandler)
]
settings = dict(
website_title=u"FoBot",
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
xsrf_cookies=True,
ui_modules={"BlogEntry": BlogEntryModule},
cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
login_url="/auth/login",
debug=True,
)
super(FoWeb, self).__init__(handlers, **settings)
def get_task(self):
return asyncio.ensure_future(tornado.ioloop.Future())

137
web/static/base.css Normal file
View File

@ -0,0 +1,137 @@
body {
background: white;
color: black;
margin: 15px;
margin-top: 0;
}
body,
input,
textarea {
font-family: Georgia, serif;
font-size: 12pt;
}
table {
border-collapse: collapse;
border: 0;
}
td {
border: 0;
padding: 0;
}
h1,
h2,
h3,
h4 {
font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
margin: 0;
}
h1 {
font-size: 20pt;
}
pre,
code {
font-family: monospace;
color: #060;
}
pre {
margin-left: 1em;
padding-left: 1em;
border-left: 1px solid silver;
line-height: 14pt;
}
a,
a code {
color: #00c;
}
#body {
max-width: 800px;
margin: auto;
}
#header {
background-color: #3b5998;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
margin-bottom: 1em;
}
#header,
#header a {
color: white;
}
#header h1 a {
text-decoration: none;
}
#footer,
#content {
margin-left: 10px;
margin-right: 10px;
}
#footer {
margin-top: 3em;
}
.entry h1 a {
color: black;
text-decoration: none;
}
.entry {
margin-bottom: 2em;
}
.entry .date {
margin-top: 3px;
}
.entry p {
margin: 0;
margin-bottom: 1em;
}
.entry .body {
margin-top: 1em;
line-height: 16pt;
}
.compose td {
vertical-align: middle;
padding-bottom: 5px;
}
.compose td.field {
padding-right: 10px;
}
.compose .title,
.compose .submit {
font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
font-weight: bold;
}
.compose .title {
font-size: 20pt;
}
.compose .title,
.compose .markdown {
width: 100%;
}
.compose .markdown {
height: 500px;
line-height: 16pt;
}

View File

@ -0,0 +1,11 @@
{% extends "../base.html" %}
{% block body %}
<form action="/auth/create" method="POST">
Email: <input name="email" type="text" required><br>
Name: <input name="name" type="text" required><br>
Password: <input name="password" type="password" required><br>
{% module xsrf_form_html() %}
<input type="submit">
</form>
{% end %}

View File

@ -0,0 +1,19 @@
{% extends "../base.html" %}
{% block title %}
{{ escape(handler.settings["website_title"]) }} - {{ _("Connection") }}
{% end %}
{% block body %}
{% if error %}
<span style="color: red">Error: {{ error }}</span><p>
{% end %}
<form action="/auth/login" method="POST">
Email: <input name="email" type="text" required><br>
Password: <input name="password" type="password" required><br>
{% module xsrf_form_html() %}
<input type="submit">
</form>
New here? <a href="/auth/create?next={{ get_arg("next","") }}">Create account</a>
{% end %}

31
web/templates/base.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- Website title -->
<title>{% block title %} {{ escape(handler.settings["website_title"]) }} {% end %}</title>
<link rel="stylesheet" href="{{ static_url("base.css") }}" type="text/css">
{% block head %}{% end %}
</head>
<body>
<div id="body">
<div id="header">
<div style="float:right">
<a href="/blog">{{ _("Blog") }}</a>
{% if current_user %}
<a href="/bot/configure">{{ _("Configure") }}</a>
<a href="/auth/logout?next={{ url_escape(request.uri) }}">{{ _("Sign out") }}</a>
{% if current_user.author %}
<a href="/blog/compose">{{ _("Compose") }}</a>
{% end %}
{% else %}
<a href="/auth/login?next={{ url_escape(request.uri) }}">{{ _("Sign in") }}</a> or <a href="/auth/create?next={{ url_escape(request.uri) }}">{{ _("create account") }}</a> to configure your bot.
{% end %}
</div>
<h1><a href="/">{% block title %}{{ escape(handler.settings["website_title"]) }}{% end %}</a></h1>
</div>
<div id="content">{% block body %}{% end %}</div>
</div>
{% block bottom %}{% end %}
</body>
</html>

View File

@ -0,0 +1,30 @@
{% extends "..\base.html" %}
{% block body %}
{% if user.author %}
<form action="{{ request.path }}" method="post" onsubmit="return validateForm()" class="compose">
<div style="margin-bottom:5px">
<input name="title" type="text" class="title" value="{{ entry.title if entry else "" }}" required/>
</div>
<div style="margin-bottom:5px">
<textarea name="markdown" rows="30" cols="40" class="markdown" required>
{{ entry.markdown if entry else "" }}
</textarea>
</div>
<div>
<input type="submit" value="{{ _("Save changes") if entry else _("Publish post") }}" class="submit"/>
&nbsp;<a href="{{ "/blog/entry/" + entry.slug if entry else "/" }}">{{ _("Cancel") }}</a>
</div>
{% if entry %}
<input type="hidden" name="id" value="{{ entry.id }}"/>
{% end %}
{% module xsrf_form_html() %}
</form>
{% else %}
<p style="color: red">You don't have right to edit or create post</p>
{% end %}
{% end %}
{% block bottom %}
{% end %}

View File

@ -0,0 +1,5 @@
{% extends "../base.html" %}
{% block body %}
{% module BlogEntry(entry) %}
{% end %}

View File

@ -0,0 +1,17 @@
{% extends "../base.html" %}
{% block title %} {{ escape(handler.settings["website_title"]) }} - Blog{% end %}
{% block body %}
{% for entry in entries %}
<div class="entry">
<h1><a href="/blog/entry/{{ entry.slug }}">{{ entry.title }}</a></h1>
<div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
<div class="body">{% raw entry.html %}</div>
{% if current_user and current_user.author %}
<div class="admin"><a href="/blog/compose?id={{ entry.id }}">{{ _("Editer le post") }}</a></div>
{% end %}
</div>
{% end %}
<div><a href="/blog/archive">{{ _("Archive") }}</a></div>
{% end %}

View File

@ -0,0 +1,8 @@
<div class="entry">
<h1><a href="/blog/entry/{{ entry.slug }}">{{ entry.title }}</a></h1>
<div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
<div class="body">{% raw entry.html %}</div>
{% if current_user.author %}
<div class="admin"><a href="/blog/compose?id={{ entry.id }}">{{ _("Editer le post") }}</a></div>
{% end %}
</div>

5
web/templates/index.html Normal file
View File

@ -0,0 +1,5 @@
{% extends base.html %}
{% block body %}
Salut
{% end %}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ titre }}</title>
</head>
<body>
{% for msg in messages %}
<div>
<h3>{{ msg.author.name }}</h3>
<p>{{ escape(msg.content) }}</p>
</div>
{% end %}
</body>
</html>