From 13709a205904b80c1bab541aaf356e32b8efe3a2 Mon Sep 17 00:00:00 2001 From: louis chauvet Date: Mon, 8 Oct 2018 23:41:54 +0200 Subject: [PATCH] First commit for web control --- Pipfile | 3 + Pipfile.lock | 70 +++++++- bot/fobot.py | 152 ++++++++++++++++ {modules => bot/modules}/config.py | 2 +- {modules => bot/modules}/github.py | 3 +- {modules => bot/modules}/help.py | 3 +- {modules => bot/modules}/modules.py | 2 +- {modules => bot/modules}/pi.py | 5 +- {modules => bot/modules}/pi/pi1.txt | 0 {modules => bot/modules}/tools.py | 2 +- traductions.py => bot/traductions.py | 0 log_config.json | 127 +++++++------ main.py | 151 ++-------------- web/server.py | 249 ++++++++++++++++++++++++++ web/static/base.css | 137 ++++++++++++++ web/templates/auth/create_user.html | 11 ++ web/templates/auth/login.html | 19 ++ web/templates/base.html | 31 ++++ web/templates/blog/compose.html | 30 ++++ web/templates/blog/entry.html | 5 + web/templates/blog/index.html | 17 ++ web/templates/blog/modules/entry.html | 8 + web/templates/index.html | 5 + web/templates/viewMsg.html | 15 ++ 24 files changed, 840 insertions(+), 207 deletions(-) create mode 100644 bot/fobot.py rename {modules => bot/modules}/config.py (99%) rename {modules => bot/modules}/github.py (93%) rename {modules => bot/modules}/help.py (98%) rename {modules => bot/modules}/modules.py (99%) rename {modules => bot/modules}/pi.py (98%) rename {modules => bot/modules}/pi/pi1.txt (100%) rename {modules => bot/modules}/tools.py (96%) rename traductions.py => bot/traductions.py (100%) create mode 100644 web/server.py create mode 100644 web/static/base.css create mode 100644 web/templates/auth/create_user.html create mode 100644 web/templates/auth/login.html create mode 100644 web/templates/base.html create mode 100644 web/templates/blog/compose.html create mode 100644 web/templates/blog/entry.html create mode 100644 web/templates/blog/index.html create mode 100644 web/templates/blog/modules/entry.html create mode 100644 web/templates/index.html create mode 100644 web/templates/viewMsg.html diff --git a/Pipfile b/Pipfile index 2a1f1d4..0a5af1b 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,9 @@ name = "pypi" "discord.py" = {ref = "rewrite", git = "https://github.com/Rapptz/discord.py"} mysql-connector-python = "*" pymysql = "*" +tornado = "*" +bcrypt = "*" +markdown = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index c1349f5..a6d2570 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0a45806745c14c2eb4a5190e94d4093508347d44f685b6b7262259c46b5f7cb5" + "sha256": "12ccc168a0520cd43d8a2ca05e3cbe21dd5262fd5a48e4eab57d368f713cb0c7" }, "pipfile-spec": 6, "requires": { @@ -23,6 +23,46 @@ ], "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": { "hashes": [ "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", @@ -86,7 +126,7 @@ }, "discord.py": { "git": "https://github.com/Rapptz/discord.py", - "ref": "00a659c6526b2445162b52eaf970adbd22c6d35d" + "ref": "418048b98abef627f57f9e28e268bf3a8668648a" }, "fs.dropboxfs": { "git": "https://github.com/rkhwaja/fs.dropboxfs.git", @@ -99,6 +139,14 @@ ], "version": "==2.7" }, + "markdown": { + "hashes": [ + "sha256:80f44d67c4f34db6ae8210a7194c7335923744181b6240e06d67479478eb7bb9", + "sha256:b853a125f03db3f2fdbcbc96fb738f2a7f2cdabc3f1262a4d89121c6ce1bd7e3" + ], + "index": "pypi", + "version": "==3.0" + }, "mysql-connector-python": { "hashes": [ "sha256:35a8f77b90d40cbf5bbb87bcfae02d63ca0383833187142ead963b1ad95ee958", @@ -142,9 +190,10 @@ }, "pycparser": { "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": { "hashes": [ @@ -160,6 +209,19 @@ "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" ], "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": {} diff --git a/bot/fobot.py b/bot/fobot.py new file mode 100644 index 0000000..1fbb5e2 --- /dev/null +++ b/bot/fobot.py @@ -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) diff --git a/modules/config.py b/bot/modules/config.py similarity index 99% rename from modules/config.py rename to bot/modules/config.py index b46028e..c450ba3 100644 --- a/modules/config.py +++ b/bot/modules/config.py @@ -1,5 +1,5 @@ import discord -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/modules/github.py b/bot/modules/github.py similarity index 93% rename from modules/github.py rename to bot/modules/github.py index 0b3dddf..4e3ab7b 100644 --- a/modules/github.py +++ b/bot/modules/github.py @@ -1,5 +1,4 @@ -import discord -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/modules/help.py b/bot/modules/help.py similarity index 98% rename from modules/help.py rename to bot/modules/help.py index e886b30..6bfc5cb 100644 --- a/modules/help.py +++ b/bot/modules/help.py @@ -1,5 +1,4 @@ -import discord -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/modules/modules.py b/bot/modules/modules.py similarity index 99% rename from modules/modules.py rename to bot/modules/modules.py index 8114084..0e9c4c1 100644 --- a/modules/modules.py +++ b/bot/modules/modules.py @@ -1,5 +1,5 @@ import discord -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/modules/pi.py b/bot/modules/pi.py similarity index 98% rename from modules/pi.py rename to bot/modules/pi.py index b6c9d1e..a8401bd 100644 --- a/modules/pi.py +++ b/bot/modules/pi.py @@ -1,9 +1,6 @@ -import os import re -import fs - -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/modules/pi/pi1.txt b/bot/modules/pi/pi1.txt similarity index 100% rename from modules/pi/pi1.txt rename to bot/modules/pi/pi1.txt diff --git a/modules/tools.py b/bot/modules/tools.py similarity index 96% rename from modules/tools.py rename to bot/modules/tools.py index 055af36..8c5b6a4 100644 --- a/modules/tools.py +++ b/bot/modules/tools.py @@ -1,7 +1,7 @@ import time import discord -import traductions as tr +from bot import traductions as tr class MainClass: diff --git a/traductions.py b/bot/traductions.py similarity index 100% rename from traductions.py rename to bot/traductions.py diff --git a/log_config.json b/log_config.json index 00e0c27..91e469e 100644 --- a/log_config.json +++ b/log_config.json @@ -1,59 +1,74 @@ { - "version": 1, - "disable_existing_loggers": false, - "formatters": { - "simple": { - "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"] + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": { + "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" + ] + }, + "webserver": { + "level": "DEBUG", + "handlers": [ + "console", + "info_file_handler", + "error_file_handler" + ] + } + }, + "root": { + "level": "INFO", + "handlers": [ + "console", + "info_file_handler", + "error_file_handler" + ] + } } \ No newline at end of file diff --git a/main.py b/main.py index 01c53be..436dc7a 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,13 @@ -import importlib +import asyncio import json import logging import logging.config -import re -import discord +import tornado.ioloop +import tornado.web + +from bot.fobot import FoBot +from web.server import FoWeb + import pymysql as mariadb @@ -61,146 +65,21 @@ def setup_logging(default_path='log_config.json', default_level=logging.INFO, en 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): - super().__init__(*args, **kwargs) - self.config_folder = config - self.config = {"guilds": {}} - self.guilds_class = {} - self.modules = {} - self.load_modules() - self.database = db_connection +eventloop = asyncio.get_event_loop() - def load_modules(self): - 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}) +foBot = FoBot(db_connection=db_connection) - def load_config(self): - for guild in self.guilds: - self.guilds_class.update({guild.id: Guild(self, guild.id)}) +foWeb = FoWeb(bot=None, db=db_connection) - def save_config(self): - pass +bot_app = foBot.start(os.environ['DISCORD_TOKEN'], max_messages=100000000) +bot_task = asyncio.ensure_future(bot_app) - async def on_connect(self): - info("foBot is connected.") +foWeb.listen(port=8888) +web_task = foWeb.get_task() - 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) - - -myBot = FoBot() -myBot.run(os.environ['DISCORD_TOKEN'], max_messages=100000000) +eventloop.run_forever() diff --git a/web/server.py b/web/server.py new file mode 100644 index 0000000..4e9b744 --- /dev/null +++ b/web/server.py @@ -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()) diff --git a/web/static/base.css b/web/static/base.css new file mode 100644 index 0000000..712959f --- /dev/null +++ b/web/static/base.css @@ -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; +} \ No newline at end of file diff --git a/web/templates/auth/create_user.html b/web/templates/auth/create_user.html new file mode 100644 index 0000000..2770e9f --- /dev/null +++ b/web/templates/auth/create_user.html @@ -0,0 +1,11 @@ +{% extends "../base.html" %} + +{% block body %} +
+ Email:
+ Name:
+ Password:
+ {% module xsrf_form_html() %} + +
+{% end %} \ No newline at end of file diff --git a/web/templates/auth/login.html b/web/templates/auth/login.html new file mode 100644 index 0000000..1935370 --- /dev/null +++ b/web/templates/auth/login.html @@ -0,0 +1,19 @@ +{% extends "../base.html" %} + +{% block title %} + {{ escape(handler.settings["website_title"]) }} - {{ _("Connection") }} +{% end %} + +{% block body %} + {% if error %} + Error: {{ error }}

+ {% end %} + +

+ Email:
+ Password:
+ {% module xsrf_form_html() %} + +
+ New here? Create account +{% end %} \ No newline at end of file diff --git a/web/templates/base.html b/web/templates/base.html new file mode 100644 index 0000000..23cfc34 --- /dev/null +++ b/web/templates/base.html @@ -0,0 +1,31 @@ + + + + + + {% block title %} {{ escape(handler.settings["website_title"]) }} {% end %} + + {% block head %}{% end %} + + +
+ +
{% block body %}{% end %}
+
+{% block bottom %}{% end %} + + \ No newline at end of file diff --git a/web/templates/blog/compose.html b/web/templates/blog/compose.html new file mode 100644 index 0000000..c9c0ca2 --- /dev/null +++ b/web/templates/blog/compose.html @@ -0,0 +1,30 @@ +{% extends "..\base.html" %} + +{% block body %} + {% if user.author %} +
+
+ +
+
+ +
+
+ +  {{ _("Cancel") }} +
+ {% if entry %} + + {% end %} + {% module xsrf_form_html() %} +
+ {% else %} +

You don't have right to edit or create post

+ {% end %} +{% end %} + +{% block bottom %} + +{% end %} \ No newline at end of file diff --git a/web/templates/blog/entry.html b/web/templates/blog/entry.html new file mode 100644 index 0000000..8708fa5 --- /dev/null +++ b/web/templates/blog/entry.html @@ -0,0 +1,5 @@ +{% extends "../base.html" %} + +{% block body %} + {% module BlogEntry(entry) %} +{% end %} \ No newline at end of file diff --git a/web/templates/blog/index.html b/web/templates/blog/index.html new file mode 100644 index 0000000..d909f7c --- /dev/null +++ b/web/templates/blog/index.html @@ -0,0 +1,17 @@ +{% extends "../base.html" %} + +{% block title %} {{ escape(handler.settings["website_title"]) }} - Blog{% end %} + +{% block body %} +{% for entry in entries %} +
+

{{ entry.title }}

+
{{ locale.format_date(entry.published, full_format=True, shorter=True) }}
+
{% raw entry.html %}
+ {% if current_user and current_user.author %} + + {% end %} +
+{% end %} +
{{ _("Archive") }}
+{% end %} \ No newline at end of file diff --git a/web/templates/blog/modules/entry.html b/web/templates/blog/modules/entry.html new file mode 100644 index 0000000..f40f3df --- /dev/null +++ b/web/templates/blog/modules/entry.html @@ -0,0 +1,8 @@ +
+

{{ entry.title }}

+
{{ locale.format_date(entry.published, full_format=True, shorter=True) }}
+
{% raw entry.html %}
+ {% if current_user.author %} + + {% end %} +
\ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..53b4eda --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,5 @@ +{% extends base.html %} + +{% block body %} + Salut +{% end %} \ No newline at end of file diff --git a/web/templates/viewMsg.html b/web/templates/viewMsg.html new file mode 100644 index 0000000..0d4dd88 --- /dev/null +++ b/web/templates/viewMsg.html @@ -0,0 +1,15 @@ + + + + + {{ titre }} + + + {% for msg in messages %} +
+

{{ msg.author.name }}

+

{{ escape(msg.content) }}

+
+ {% end %} + + \ No newline at end of file