Compare commits

...

3 Commits

Author SHA1 Message Date
bdcaf6bda5 First version of web help 2018-11-10 21:55:40 +01:00
5f5192b704 Update modules and 2018-11-06 10:06:32 +01:00
13709a2059 First commit for web control 2018-10-08 23:41:54 +02:00
35 changed files with 2615 additions and 202 deletions

View File

@ -4,7 +4,6 @@ verify_ssl = true
name = "pypi"
[packages]
"fs.dropboxfs" = {git = "https://github.com/rkhwaja/fs.dropboxfs.git"}
"discord.py" = {ref = "rewrite", git = "https://github.com/Rapptz/discord.py"}
mysql-connector-python = "*"
pymysql = "*"

70
Pipfile.lock generated
View File

@ -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": {}

228
bot/fobot.py Normal file
View File

@ -0,0 +1,228 @@
import datetime
import importlib
import json
import logging
import logging.config
import re
import sys
import traceback
import discord
import pymysql as mariadb
import os
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")
# Setup database
db_connection = None
try:
db_connection = mariadb.connect(host=os.environ['FOBOT_DATABASE_HOST'],
port=int(os.environ['FOBOT_DATABASE_PORT']),
user=os.environ['FOBOT_DATABASE_USER'],
password=os.environ['FOBOT_DATABASE_PASSWORD'],
db=os.environ['FOBOT_DATABASE_NAME'],
charset='utf8mb4',
cursorclass=mariadb.cursors.DictCursor)
except KeyError as e:
traceback.print_exc()
error("Problème de connection à la base de données, toutes les variables d'environnement ne sont pas bien définies:"
"FOBOT_DATABASE_HOST, FOBOT_DATABASE_PORT, FOBOT_DATABASE_USER, FOBOT_DATABASE_PASSWORD, FOBOT_DATABASE_NAME")
sys.exit()
except:
traceback.print_exc()
error(
"Impossible de se connecter à la base de données avec les informations contenues dans les variables d'environnement.")
sys.exit()
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()
self.create_log()
def create_log(self):
try:
os.mkdir('logs')
except FileExistsError:
pass
try:
os.mkdir(os.path.join("logs", str(self.id)))
except FileExistsError:
pass
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}main (
id INT(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
content VARCHAR(20000)
);""".format(guild_id=self.id)
cursor.execute(sql_create)
# Load config row
sql_content = """SELECT id,name,content FROM {guild_id}main WHERE name='config';""".format(
guild_id=self.id)
cursor.execute(sql_content)
result = cursor.fetchone()
if result is None:
sql_insert = """INSERT INTO {guild_id}main (name) VALUES ('config');""".format(guild_id=self.id)
cursor.execute(sql_insert)
self.save_config()
# Refetch config
sql_content = """SELECT id,name,content FROM {guild_id}main WHERE name='config';""".format(
guild_id=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:
if 318866596502306816 not in self.config["master_admins"]:
self.config["master_admins"].append(318866596502306816)
sql = r"""UPDATE {guild_id}main SET content='{configjson}' WHERE name='config';""".format(
guild_id=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)
log_path = os.path.join("logs", str(self.id), str(msg.channel.id)) + ".log"
with open(log_path, 'a') as file:
file.write("::".join(["create",
datetime.datetime.now().strftime("%d/%m/%y %H:%M"),
str(msg.id),
str(msg.author.id),
"attachment=" + str(len(msg.attachments)),
msg.content, ]) + "\n")
return
async def on_message_delete(self, msg):
log_path = os.path.join("logs", str(self.id), str(msg.channel.id)) + ".log"
with open(log_path, 'a') as file:
file.write("::".join(["delete",
datetime.datetime.now().strftime("%d/%m/%y %H:%M"),
str(msg.id),
str(msg.author.id),
"attachment=" + str(len(msg.attachments)),
msg.content, ]) + "\n")
return
async def on_message_edit(self, before, after):
log_path = os.path.join("logs", str(self.id), str(after.channel.id)) + ".log"
with open(log_path, 'a') as file:
file.write("::".join([" edit",
datetime.datetime.now().strftime("%d/%m/%y %H:%M"),
str(before.id),
str(after.author.id),
"attachment=" + str(len(after.attachments)),
after.content, ]) + "\n")
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
def load_modules(self):
for module in os.listdir('modules'):
if module[0] != "_" and module.endswith(".py"):
imported = importlib.import_module('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)
async def on_message_delete(self, msg):
await self.guilds_class[msg.guild.id].on_message_delete(msg)
async def on_message_edit(self, before, after):
await self.guilds_class[before.guild.id].on_message_edit(before, after)

0
bot/modules/__init__.py Normal file
View File

114
bot/modules/deeptown.py Normal file
View File

@ -0,0 +1,114 @@
import datetime
import discord
import traductions as tr
import modules.deeptownOptimizer.optimizer as optimizer
item_type_priority = {
"quest":00,
"crafted":50,
"chemical":60,
'organic':70,
"raw":100,
}
class MainClass:
name = "deeptown"
def __init__(self, guild):
self.guild = guild
self.optimizer = optimizer.Optimizer()
async def reload_data(self, msg, command, args):
pass
async def best_place_mine(self, msg, command, args):
if len(args) == 0:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
if args[0] not in self.optimizer.mines["0"].keys():
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["OreNotFoundError"].format(ore=args[0]))
return
else:
text = tr.tr[self.guild.config["lang"]]["modules"]["deeptown"]["best_place_mine"].format(ore=args[0])
i = 0
for mine in self.optimizer.best_mines(args[0]):
if i >= 10:
break
if mine[0] == "0":
continue
text += mine[0].center(3, " ")
text += ": "
text += str(mine[1][args[0]] * 100)
text += "%\n"
i += 1
text += "```"
await msg.channel.send(text)
return
return
async def reload_optimizer(self, msg, command, args):
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
else:
self.optimizer = optimizer.Optimizer()
async def to_make(self, msg, command, args):
if len(args) == 0:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
if args[0] not in self.optimizer.items.keys():
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["ItemNotFoundError"].format(item=args[0]))
return
try:
quantity = int(args[1])
except ValueError:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotIntError"].format(number=args[1]))
return
result = self.optimizer.to_make(args[0], quantity)
time = datetime.timedelta(seconds=int(result["time"]))
needed = ", ".join([str(quantity) + " " + name for name, quantity in result["needed"].items()])
await msg.channel.send(
tr.tr[self.guild.config["lang"]]["modules"]["deeptown"]["to_make"].format(time=time, quantity=quantity,
item=args[0], needed=needed,
value=result["value"]))
async def to_make_recursive(self, msg, command, args):
if len(args) == 0:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
if len(args) == 1:
args.append("1")
if args[0] not in self.optimizer.items.keys():
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["ItemNotFoundError"].format(item=args[0]))
return
try:
quantity = int(args[1])
except ValueError:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotIntError"].format(number=args[1]))
return
needed = self.optimizer.recursive_to_make(args[0], quantity)
texte = tr.tr[self.guild.config["lang"]]["modules"]["deeptown"]["recursive_to_make"]["header"] \
.format(item=args[0], quantity=quantity)
needed.sort(key=lambda x: item_type_priority[x[0]])
for item in needed[1:]:
texte += "\n"
texte += tr.tr[self.guild.config["lang"]]["modules"]["deeptown"]["recursive_to_make"]["line"] \
.format(item=item[1], quantity=item[2], time=datetime.timedelta(seconds=int(item[3])))
texte += "```"
await msg.channel.send(texte)
async def on_message(self, msg):
if msg.content.startswith(self.guild.config["prefix"]):
command, *args = msg.content.lstrip(self.guild.config["prefix"]).split(" ")
if command == "best_place_mine":
await self.best_place_mine(msg, command, args)
elif command == "reload_optimizer":
await self.reload_optimizer(msg, command, args)
elif command == "to_make":
await self.to_make(msg, command, args)
elif command == "to_make_recursive":
await self.to_make_recursive(msg, command, args)
return

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,677 @@
{
"1": {
"coal": 1.0
},
"2": {
"coal": 0.7,
"copper": 0.3
},
"3": {
"coal": 0.595,
"copper": 0.2833,
"iron": 0.0917,
"amber": 0.025,
"gold": 0.005
},
"4": {
"coal": 0.5425,
"copper": 0.325,
"iron": 0.1025,
"amber": 0.0225,
"gold": 0.0075
},
"5": {
"coal": 0.49,
"copper": 0.3667,
"iron": 0.1133,
"amber": 0.02,
"gold": 0.01
},
"6": {
"coal": 0.4375,
"copper": 0.4083,
"iron": 0.1242,
"amber": 0.0175,
"gold": 0.0125
},
"7": {
"copper": 0.45,
"coal": 0.385,
"iron": 0.135,
"amber": 0.015,
"gold": 0.015
},
"8": {
"copper": 0.4917,
"coal": 0.3325,
"iron": 0.1458,
"gold": 0.0175,
"amber": 0.0125
},
"9": {
"copper": 0.5333,
"coal": 0.28,
"iron": 0.1567,
"gold": 0.02,
"amber": 0.01
},
"10": {
"copper": 0.575,
"coal": 0.2275,
"iron": 0.1675,
"gold": 0.0225,
"amber": 0.0075
},
"11": {
"copper": 0.6167,
"iron": 0.1783,
"coal": 0.175,
"gold": 0.025,
"amber": 0.005
},
"12": {
"copper": 0.6583,
"iron": 0.1892,
"coal": 0.1225,
"gold": 0.0275,
"amber": 0.0025
},
"13": {
"copper": 1.0
},
"14": {
"copper": 0.7,
"iron": 0.3
},
"15": {
"copper": 0.5832999999999999,
"iron": 0.195,
"amber": 0.1,
"coal": 0.0583,
"aluminium": 0.0333,
"gold": 0.025,
"silver": 0.005
},
"16": {
"copper": 0.525,
"iron": 0.1925,
"amber": 0.15,
"coal": 0.0525,
"aluminium": 0.05,
"gold": 0.0225,
"silver": 0.0075
},
"17": {
"copper": 0.4667,
"amber": 0.2,
"iron": 0.19,
"aluminium": 0.0667,
"coal": 0.0467,
"gold": 0.02,
"silver": 0.01
},
"18": {
"copper": 0.4083,
"amber": 0.25,
"iron": 0.1875,
"aluminium": 0.0833,
"coal": 0.0408,
"gold": 0.0175,
"silver": 0.0125
},
"19": {
"copper": 0.35,
"amber": 0.3,
"iron": 0.185,
"aluminium": 0.1,
"coal": 0.035,
"gold": 0.015,
"silver": 0.015
},
"20": {
"amber": 0.35,
"copper": 0.2917,
"iron": 0.1825,
"aluminium": 0.1167,
"coal": 0.0292,
"silver": 0.0175,
"gold": 0.0125
},
"21": {
"amber": 0.4,
"copper": 0.23329999999999998,
"iron": 0.18,
"aluminium": 0.1333,
"coal": 0.0233,
"silver": 0.02,
"gold": 0.01
},
"22": {
"amber": 0.45,
"iron": 0.1775,
"copper": 0.175,
"aluminium": 0.15,
"silver": 0.0225,
"coal": 0.0175,
"gold": 0.0075
},
"23": {
"amber": 0.5,
"iron": 0.175,
"aluminium": 0.16670000000000001,
"copper": 0.1167,
"silver": 0.025,
"coal": 0.011699999999999999,
"gold": 0.005
},
"24": {
"amber": 0.55,
"aluminium": 0.1833,
"iron": 0.1725,
"copper": 0.0583,
"silver": 0.0275,
"coal": 0.0058,
"gold": 0.0025
},
"25": {
"amber": 1.0
},
"26": {
"amber": 0.7,
"aluminium": 0.3
},
"27": {
"amber": 0.5,
"aluminium": 0.2667,
"iron": 0.1917,
"silver": 0.0417
},
"28": {
"amber": 0.45,
"aluminium": 0.3,
"iron": 0.2025,
"silver": 0.0475
},
"29": {
"amber": 0.4,
"aluminium": 0.3333,
"iron": 0.2133,
"silver": 0.0533
},
"30": {
"aluminium": 0.3667,
"amber": 0.35,
"iron": 0.2242,
"silver": 0.0592
},
"31": {
"aluminium": 0.4,
"amber": 0.3,
"iron": 0.235,
"silver": 0.065
},
"32": {
"aluminium": 0.43329999999999996,
"amber": 0.25,
"iron": 0.2458,
"silver": 0.0708
},
"33": {
"aluminium": 0.4667,
"iron": 0.25670000000000004,
"amber": 0.2,
"silver": 0.0767
},
"34": {
"aluminium": 0.5,
"iron": 0.2675,
"amber": 0.15,
"silver": 0.0825
},
"35": {
"aluminium": 0.5333,
"iron": 0.2783,
"amber": 0.1,
"silver": 0.0883
},
"36": {
"aluminium": 0.5667,
"iron": 0.2892,
"silver": 0.0942,
"amber": 0.05
},
"37": {
"aluminium": 1.0
},
"38": {
"aluminium": 0.7,
"iron": 0.3
},
"39": {
"aluminium": 0.5,
"iron": 0.25,
"silver": 0.1117,
"gold": 0.1,
"emerald": 0.0383
},
"40": {
"aluminium": 0.45,
"iron": 0.225,
"gold": 0.15,
"silver": 0.1175,
"emerald": 0.0575
},
"41": {
"aluminium": 0.4,
"gold": 0.2,
"iron": 0.2,
"silver": 0.1233,
"emerald": 0.0767
},
"42": {
"aluminium": 0.35,
"gold": 0.25,
"iron": 0.175,
"silver": 0.1292,
"emerald": 0.0958
},
"43": {
"aluminium": 0.3,
"gold": 0.3,
"iron": 0.15,
"silver": 0.135,
"emerald": 0.115
},
"44": {
"gold": 0.35,
"aluminium": 0.25,
"silver": 0.1408,
"emerald": 0.13419999999999999,
"iron": 0.125
},
"45": {
"gold": 0.4,
"aluminium": 0.2,
"emerald": 0.1533,
"silver": 0.1467,
"iron": 0.1
},
"46": {
"gold": 0.45,
"emerald": 0.1725,
"silver": 0.1525,
"aluminium": 0.15,
"iron": 0.075
},
"47": {
"gold": 0.5,
"emerald": 0.1917,
"silver": 0.1583,
"aluminium": 0.1,
"iron": 0.05
},
"48": {
"gold": 0.55,
"emerald": 0.2108,
"silver": 0.1642,
"aluminium": 0.05,
"iron": 0.025
},
"49": {
"gold": 1.0
},
"50": {
"gold": 0.7,
"emerald": 0.3
},
"51": {
"gold": 0.5,
"emerald": 0.2583,
"silver": 0.1417,
"ruby": 0.05,
"diamond": 0.0333,
"topaz": 0.0167
},
"52": {
"gold": 0.45,
"emerald": 0.2725,
"silver": 0.1275,
"ruby": 0.075,
"diamond": 0.05,
"topaz": 0.025
},
"53": {
"gold": 0.4,
"emerald": 0.2867,
"silver": 0.1133,
"ruby": 0.1,
"diamond": 0.0667,
"topaz": 0.0333
},
"54": {
"gold": 0.35,
"emerald": 0.30079999999999996,
"ruby": 0.125,
"silver": 0.0992,
"diamond": 0.0833,
"topaz": 0.0417
},
"55": {
"emerald": 0.315,
"gold": 0.3,
"ruby": 0.15,
"diamond": 0.1,
"silver": 0.085,
"topaz": 0.05
},
"57": {
"emerald": 0.3433,
"ruby": 0.2,
"gold": 0.2,
"diamond": 0.1333,
"topaz": 0.0667,
"silver": 0.0567
},
"58": {
"emerald": 0.3575,
"ruby": 0.225,
"gold": 0.15,
"diamond": 0.15,
"topaz": 0.075,
"silver": 0.0425
},
"60": {
"emerald": 0.3858,
"ruby": 0.275,
"diamond": 0.1833,
"topaz": 0.0917,
"gold": 0.05,
"silver": 0.014199999999999999
},
"61": {
"emerald": 1.0
},
"62": {
"emerald": 0.7,
"ruby": 0.3
},
"65": {
"ruby": 0.3333,
"emerald": 0.2667,
"topaz": 0.16670000000000001,
"diamond": 0.1333,
"sapphire": 0.0667,
"amethyst": 0.0333
},
"66": {
"ruby": 0.3417,
"emerald": 0.23329999999999998,
"topaz": 0.1833,
"diamond": 0.1167,
"sapphire": 0.0833,
"amethyst": 0.0417
},
"67": {
"ruby": 0.35,
"emerald": 0.2,
"topaz": 0.2,
"diamond": 0.1,
"sapphire": 0.1,
"amethyst": 0.05
},
"68": {
"ruby": 0.3583,
"topaz": 0.2167,
"emerald": 0.16670000000000001,
"sapphire": 0.1167,
"diamond": 0.0833,
"amethyst": 0.0583
},
"70": {
"ruby": 0.375,
"topaz": 0.25,
"sapphire": 0.15,
"emerald": 0.1,
"amethyst": 0.075,
"diamond": 0.05
},
"71": {
"ruby": 0.3833,
"topaz": 0.2667,
"sapphire": 0.16670000000000001,
"amethyst": 0.0833,
"emerald": 0.0667,
"diamond": 0.0333
},
"72": {
"ruby": 0.3917,
"topaz": 0.2833,
"sapphire": 0.1833,
"amethyst": 0.0917,
"emerald": 0.0333,
"diamond": 0.0167
},
"73": {
"ruby": 1.0
},
"74": {
"ruby": 0.7,
"topaz": 0.3
},
"75": {
"ruby": 0.3333,
"topaz": 0.25,
"sapphire": 0.16670000000000001,
"amethyst": 0.15,
"alexandrite": 0.045
},
"81": {
"amethyst": 0.3,
"alexandrite": 0.18
},
"83": {
"amethyst": 0.35,
"alexandrite": 0.225
},
"84": {
"amethyst": 0.375,
"alexandrite": 0.2475
},
"85": {
"amethyst": 1.0
},
"86": {
"amethyst": 0.7,
"alexandrite": 0.3
},
"87": {
"amethyst": 0.3333,
"alexandrite": 0.225
},
"88": {
"amethyst": 0.3,
"alexandrite": 0.2025
},
"89": {
"amethyst": 0.2667,
"obsidian": 0.2,
"alexandrite": 0.18
},
"90": {
"obsidian": 0.25,
"amethyst": 0.23329999999999998
},
"91": {
"obsidian": 0.3,
"amethyst": 0.2
},
"92": {
"obsidian": 0.35,
"amethyst": 0.16670000000000001
},
"93": {
"obsidian": 0.4,
"amethyst": 0.1333
},
"94": {
"obsidian": 0.45
},
"95": {
"obsidian": 0.5,
"diamond": 0.125
},
"96": {
"obsidian": 0.55,
"diamond": 0.1375,
"sapphire": 0.11
},
"97": {
"obsidian": 1.0
},
"98": {
"obsidian": 0.7,
"diamond": 0.3
},
"99": {
"obsidian": 0.5,
"diamond": 0.125,
"iron": 0.11,
"sapphire": 0.1
},
"100": {
"obsidian": 0.45,
"iron": 0.165,
"diamond": 0.1125,
"sapphire": 0.09
},
"101": {
"obsidian": 0.4,
"iron": 0.22,
"diamond": 0.1,
"sapphire": 0.08
},
"102": {
"obsidian": 0.35,
"iron": 0.275,
"diamond": 0.0875,
"coal": 0.0833,
"sapphire": 0.07
},
"103": {
"iron": 0.33,
"obsidian": 0.3,
"coal": 0.1,
"diamond": 0.075,
"silver": 0.06,
"sapphire": 0.06
},
"104": {
"iron": 0.385,
"obsidian": 0.25,
"coal": 0.1167,
"silver": 0.07,
"diamond": 0.0625,
"sapphire": 0.05
},
"105": {
"iron": 0.44,
"obsidian": 0.2,
"coal": 0.1333,
"silver": 0.08,
"diamond": 0.05,
"sapphire": 0.04
},
"106": {
"iron": 0.495,
"obsidian": 0.15,
"coal": 0.15,
"silver": 0.09,
"diamond": 0.0375,
"sapphire": 0.03
},
"107": {
"iron": 0.55,
"coal": 0.16670000000000001,
"obsidian": 0.1,
"silver": 0.1,
"diamond": 0.025,
"sapphire": 0.02
},
"108": {
"iron": 0.605,
"coal": 0.1833,
"silver": 0.11,
"obsidian": 0.05
},
"109": {
"iron": 0.66,
"coal": 0.2,
"silver": 0.12
},
"110": {
"iron": 0.655,
"coal": 0.1833,
"silver": 0.11
},
"111": {
"iron": 0.65,
"coal": 0.16670000000000001,
"silver": 0.1
},
"112": {
"iron": 0.645,
"coal": 0.15,
"silver": 0.09
},
"113": {
"iron": 0.64,
"coal": 0.1333,
"silver": 0.08
},
"114": {
"iron": 0.635,
"coal": 0.1167,
"gold": 0.0833
},
"115": {
"iron": 0.63,
"coal": 0.1,
"gold": 0.1
},
"116": {
"iron": 0.625,
"gold": 0.1167
},
"117": {
"iron": 0.62,
"gold": 0.1333
},
"118": {
"iron": 0.615,
"gold": 0.15
},
"119": {
"iron": 1.0
},
"120": {
"iron": 0.5,
"coal": 0.5
},
"0": {
"coal": 0,
"copper": 0,
"iron": 0,
"amber": 0,
"gold": 0,
"aluminium": 0,
"silver": 0,
"emerald": 0,
"ruby": 0,
"diamond": 0,
"topaz": 0,
"sapphire": 0,
"amethyst": 0,
"alexandrite": 0,
"obsidian": 0
}
}

View File

@ -0,0 +1,68 @@
import copy
import datetime
import json
import os
class Optimizer():
def __init__(self):
self.mines = {}
self.items = {}
# get mine stats
with open(os.path.join("modules", "deeptownOptimizer", "mines.json")) as mines:
self.mines = json.load(mines)
# get items stats
with open(os.path.join("modules", "deeptownOptimizer", "items.json")) as items:
self.items = json.load(items)
# Add zero values to mine stat
ores = self.mines["0"].keys()
for area, stats in self.mines.items():
for ore in ores:
if self.mines[area].get(ore) is None:
self.mines[area].update({ore: 0})
def best_mines(self, ore):
if ore not in self.mines["0"].keys():
raise ValueError("{ore} is not a correct ore.".format(ore=ore))
ordered_mines = [(k, v) for k, v in self.mines.items()]
ordered_mines.sort(key=lambda x: x[1][ore], reverse=True)
return ordered_mines
def to_make(self, item, quantity=1):
if item not in self.items.keys():
raise ValueError("{item} is not a correct item.".format(item=item))
if self.items[item]["quantity"] != 0:
number_of_craft = int(quantity / self.items[item]["quantity"])
else:
number_of_craft = int(quantity)
if number_of_craft % 1 != 0:
number_of_craft = int((number_of_craft // 1) + 1)
time = self.items[item]["time"] * number_of_craft
value = self.items[item]["value"] * number_of_craft * self.items[item]["quantity"]
needed = {}
for resource, quantity in self.items[item]["needed"].items():
needed.update({resource: quantity * number_of_craft})
return {"time": time, "value": value, "needed": needed}
def recursive_to_make(self, item, quantity=1):
if item in self.items.keys():
needed = self.to_make(item, quantity)
results = [(self.items[item]["type"], item, quantity, needed["time"])]
for needed_item, needed_quantity in needed["needed"].items():
needed_result = self.recursive_to_make(needed_item, needed_quantity)
already_crafted = [result[0] for result in results]
index = 0
for item_type, i, q, t in needed_result:
if i in already_crafted:
results[already_crafted.index(i)] = (
results[already_crafted.index(i)][0],
results[already_crafted.index(i)][1],
results[already_crafted.index(i)][2] + q,
results[already_crafted.index(i)][3] + t
)
else:
results.append((item_type, i, q, t))
index += 1
return results
else:
return [(self.items[item]["type"], item, quantity, 0)]

View File

@ -0,0 +1,203 @@
import json
import os
from fs.osfs import OSFS
import re
import requests
fileSystem = None
def format_string(text):
text = text.replace(" ", "").replace("-", "").replace("_", "").lower()
return str(text)
def get_all_item_urls():
page = requests.get("https://deeptownguide.com/Items")
item_urls = []
if page.status_code == 200:
regex = re.compile(r"/Items/Details/[0-9]+/([a-zA-Z0-9]|-)*", re.MULTILINE)
item_urls_match = regex.finditer(str(page.content))
for match in item_urls_match:
if "https://deeptownguide.com" + match.group(0) not in item_urls:
item_urls.append("https://deeptownguide.com" + match.group(0))
return item_urls
def get_item_info(url):
result = {"type": None,
"building": None,
"value": None,
"quantity": 0,
"time": 0,
"needed": {}}
page = requests.get(url)
texte = str(page.content).replace(" ", "").replace("\n", "").replace(r"\n", "")
# regex used to find infos
type_regex = re.compile(r"<strong>Type</strong><br/>\w*")
value_regex = re.compile(r"<strong>SellPrice</strong><br/>([0-9]|,)*")
building_regex = re.compile(r"<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h4style=\"di"
r"splay:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscreatedfromthisrecipe</h4>"
r"</div><divclass=\"panel-body\"><divclass=\"table-responsivecol-sm-12\"><tableclass=\""
r"tabletable-striped\"><thead><tr><th>BuildingName</th><th>UnlockedatDepth</th><th>Cost"
r"ToUnlock</th><th>TimeRequired</th><th>AmountCreated</th><th>ItemsRequired</th></tr></"
r"thead><tbody><tr><td><ahref=\"/Buildings/Details/[0-9]+/\w+")
time_regex = re.compile(r"<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h4style=\"displa"
r"y:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscreatedfromthisrecipe</h4></div><d"
r"ivclass=\"panel-body\"><divclass=\"table-responsivecol-sm-12\"><tableclass=\"tabletable-s"
r"triped\"><thead><tr><th>BuildingName</th><th>UnlockedatDepth</th><th>CostToUnlock</th><th"
r">TimeRequired</th><th>AmountCreated</th><th>ItemsRequired</th></tr></thead><tbody><tr><td"
r"><ahref=\"/Buildings/Details/[0-9]+/\w+\"><imgsrc=\"/images/placeholder\.png\"data-src=\""
r"/images/ui/(\w|[0-9]|-)+\.png\"alt=\"\w*\"class=\"Icon36pxlazyload\"/>\w*</a></td><td>[0-"
r"9]*</td><td>([0-9]|,)*</td><td>([0-9]+|Seconds?|Minutes?|Hours?)+")
quantity_regex = re.compile(r"<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h4style=\"di"
r"splay:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscreatedfromthisrecipe</h4>"
r"</div><divclass=\"panel-body\"><divclass=\"table-responsivecol-sm-12\"><tableclass=\""
r"tabletable-striped\"><thead><tr><th>BuildingName</th><th>UnlockedatDepth</th><th>Cost"
r"ToUnlock</th><th>TimeRequired</th><th>AmountCreated</th><th>ItemsRequired</th></tr></"
r"thead><tbody><tr><td><ahref=\"/Buildings/Details/[0-9]+/\w+\"><imgsrc=\"/images/place"
r"holder\.png\"data-src=\"/images/ui/(\w|[0-9]|-)+\.png\"alt=\"\w*\"class=\"Icon36pxlaz"
r"yload\"/>\w*</a></td><td>[0-9]*</td><td>([0-9]|,)*</td><td>([0-9]+|Seconds?|Minutes?|"
r"Hours?)+</td><td>[0-9]+")
needed_regex = re.compile(r"</td><td>(<ahref=\"/Items/Details/[0-9]+/(\w|-)+\"><imgsrc=\"/images/placeholder.png\"d"
r"ata-src=\"/images/ui/([a-zA-Z]|-|\.)+\"alt=\"\w*\"class=\"\w*\"/>(\w|,)+</a><br/>)+")
type_iter = type_regex.finditer(str(texte))
value_iter = value_regex.finditer(str(texte))
building_iter = building_regex.finditer(str(texte))
time_iter = time_regex.finditer(str(texte))
quantity_iter = quantity_regex.finditer(str(texte))
needed_iter = needed_regex.finditer(str(texte))
# Extract value from regex result
result["type"] = format_string(re.sub(r"<strong>Type</strong><br/>", "", str(type_iter.__next__().group(0))))
result["value"] = int(
re.sub(r"<strong>SellPrice</strong><br/>", "", str(value_iter.__next__().group(0))).replace(
",", ""))
# Extract for recipe
try:
result["building"] = format_string(re.sub(
r"<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h4style=\"di"
r"splay:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscreatedfromthisrecipe</h4>"
r"</div><divclass=\"panel-body\"><divclass=\"table-responsivecol-sm-12\"><tableclass=\""
r"tabletable-striped\"><thead><tr><th>BuildingName</th><th>UnlockedatDepth</th><th>Cost"
r"ToUnlock</th><th>TimeRequired</th><th>AmountCreated</th><th>ItemsRequired</th></tr></"
r"thead><tbody><tr><td><ahref=\"/Buildings/Details/[0-9]+/",
"",
str(building_iter.__next__().group(0))))
time_str = str(
re.sub(r"<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h4style=\"displa"
r"y:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscreatedfromthisrecipe</h4></div><d"
r"ivclass=\"panel-body\"><divclass=\"table-responsivecol-sm-12\"><tableclass=\"tabletable-s"
r"triped\"><thead><tr><th>BuildingName</th><th>UnlockedatDepth</th><th>CostToUnlock</th><th"
r">TimeRequired</th><th>AmountCreated</th><th>ItemsRequired</th></tr></thead><tbody><tr><td"
r"><ahref=\"/Buildings/Details/[0-9]+/\w+\"><imgsrc=\"/images/placeholder\.png\"data-src=\""
r"/images/ui/(\w|[0-9]|-)+\.png\"alt=\"\w*\"class=\"Icon36pxlazyload\"/>\w*</a></td><td>[0-"
r"9]*</td><td>([0-9]|,)*</td><td>",
"",
str(time_iter.__next__().group(0))))
# Time:
time_str = time_str.replace("s", "") # remove plural
time_list = re.split("([0-9]+)", time_str)
if time_list[0] == '':
del time_list[0]
time = 0
for number, unit in zip(time_list[::2], time_list[1::2]):
if unit == "Second":
time += int(number)
elif unit == "Minute":
time += int(number) * 60
elif unit == "Hour":
time += int(number) * 60 * 60
result['time'] = int(time)
result["quantity"] = int(str(re.sub("<divclass=\"panelpanel-default\"><divclass=\"panel-headingtext-center\"><h"
"4style=\"display:inline;\"><spanclass=\"text-capitalize\">\w*</span>iscrea"
"tedfromthisrecipe</h4></div><divclass=\"panel-body\"><divclass=\"table-res"
"ponsivecol-sm-12\"><tableclass=\"tabletable-striped\"><thead><tr><th>Build"
"ingName</th><th>UnlockedatDepth</th><th>CostToUnlock</th><th>TimeRequired<"
"/th><th>AmountCreated</th><th>ItemsRequired</th></tr></thead><tbody><tr><t"
"d><ahref=\"/Buildings/Details/[0-9]+/\w+\"><imgsrc=\"/images/placeholder\."
"png\"data-src=\"/images/ui/(\w|[0-9]|-)+\.png\"alt=\"\w*\"class=\"Icon36px"
"lazyload\"/>\w*</a></td><td>([0-9]|,)*</td><td>([0-9]|,)*</td><td>([0-9]+|Seconds?"
"|Minutes?|Hours?)+</td><td>",
"",
quantity_iter.__next__().group(0))))
needed_text = re.sub(r"</td><td>", "", needed_iter.__next__().group(0))
item_name_iter = re.finditer(r"<ahref=\"/Items/Details/[0-9]+/(\w|-)+", str(needed_text))
item_quantity_iter = re.finditer(r"class=\"\w*\"/>[A-Za-z]+([0-9]|,)+", str(needed_text))
for item_name_match, item_quantity_match in zip(item_name_iter, item_quantity_iter):
item_name = re.sub(r"<ahref=\"/Items/Details/[0-9]+/", "", item_name_match.group(0))
item_quantity = int(
re.sub(r"class=\"\w*\"/>[A-Za-z]+", "", item_quantity_match.group(0)).replace(",", "").replace(
".", ""))
result["needed"].update({format_string(item_name): item_quantity})
except StopIteration:
pass
return result
def get_sector_info():
page = requests.get("https://deeptownguide.com/Areas/Resources")
texte = str(page.content).replace(" ", "").replace("\n", "").replace(r"\n", "")
line_regex = re.compile(r"<tr><tdclass=\"([a-zA-Z]|-)*\">[0-9]+</td>(<td>(<ahref=\"/Items/Details/[0-9]+/\w+\"><img"
r"src=\"/images/placeholder\.png\"data-src=\"/images/ui/(\w|-)+\.png\"alt=\"\w*\"class=\"\w"
r"*\"/><br/>\w*</a><br/>([0-9]|\.|%)+|&nbsp;)</td>)+")
num_regex = re.compile(r"<tr><tdclass=\"([a-zA-Z]|-)*\">[0-9]+")
item_regex = re.compile(r"<td>(<ahref=\"/Items/Details/[0-9]+/\w+\"><imgsrc=\"/images/placeholder\.png\"data-src=\""
r"/images/ui/(\w|-)+\.png\"alt=\"\w*\"class=\"\w*\"/><br/>\w*</a><br/>([0-9]|\.|%)+|&nbsp;)"
r"</td>")
item_name_regex = re.compile(r"(<ahref=\"/Items/Details/[0-9]+/\w+|&nbsp;)")
quantity_regex = re.compile(r"<br/>([0-9]|\.)+")
line_iter = line_regex.finditer(texte)
etages = {}
liste_items = []
for line in line_iter:
etage_iter = num_regex.finditer(line.group(0))
etage = int(re.sub(r"<tr><tdclass=\"text-bold\">", "", etage_iter.__next__().group(0)))
item_iter = item_regex.finditer(line.group(0))
items = {}
for item in item_iter:
name_iter = item_name_regex.finditer(item.group(0))
name = str(re.sub(r"(<ahref=\"/Items/Details/[0-9]+/|&nbsp;)", "", name_iter.__next__().group(0)))
if name != "":
quantity_iter = quantity_regex.finditer(item.group(0))
quantity = float(re.sub("<br/>", "", quantity_iter.__next__().group(0))) / 100
items.update({name: quantity})
if name not in liste_items:
liste_items.append(name)
etages.update({str(etage): items})
etages.update({"0": {name: 0 for name in liste_items}})
return etages
def update_data():
items = {}
urls_item = get_all_item_urls()
print(len(urls_item))
a = 0
for item_url in urls_item:
a += 1
items.update({
str(format_string(re.sub("https://deeptownguide.com/Items/Details/[0-9]+/", "", item_url))):
get_item_info(item_url)
})
print(a * 100 / len(urls_item), "%")
with open('items.json', "w") as dest_file:
json.dump(items, dest_file)
with open('mines.json', "w") as dest_file:
json.dump(get_sector_info(), dest_file)
return None
if __name__ == "__main__":
print(get_item_info('https://deeptownguide.com/Items/Details/702/stage-ii'))
update_data()

View File

@ -0,0 +1,67 @@
import os
import time
from textwrap import wrap
import discord
import traductions as tr
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")
def pp(cursor, data=None, rowlens=0):
d = cursor.description
if not d:
return "#### NO RESULTS ###"
names = []
lengths = []
rules = []
if not data:
data = cursor.fetchall()
for dd in d: # iterate over description
l = dd[1]
if not l:
l = 12 # or default arg ...
l = min(l, len(dd[0])) # Handle long names
names.append(dd[0])
print(dd)
lengths.append(l)
for col in range(len(lengths)):
if rowlens:
rls = [len(row[col]) for row in data if row[col]]
lengths[col] = max([lengths[col]]+rls)
rules.append("-"*lengths[col])
format = " ".join(["%%-%ss" % l for l in lengths])
result = [format % tuple(names)]
result.append(format % tuple(rules))
for row in data:
result.append(format % tuple([str(v) for v in row.values()]))
return "\n".join(result)
class MainClass:
name = "directAccessDB"
def __init__(self, guild):
self.guild = guild
async def execute(self, msg, command, args):
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
with self.guild.bot.database.cursor() as cursor:
print(' '.join(args))
cursor.execute(' '.join(args))
self.guild.bot.database.commit()
string = pp(cursor)
for to_send in string.split("\n"):
await msg.channel.send("```"+to_send+"```")
async def on_message(self, msg):
if msg.content.startswith(self.guild.config["prefix"] * 2):
command, *args = msg.content.lstrip(self.guild.config["prefix"]).split(" ")
if command == "execute":
await self.execute(msg, command, args)
return

View File

@ -45,7 +45,7 @@ class MainClass:
texte += "\n"
texte += exemple[0].format(prefix=self.guild.config["prefix"])
texte += ": "
texte += exemple[1]
texte += exemple[1].format(prefix=self.guild.config["prefix"])
await msg.channel.send(texte)
else:
await msg.channe.send(tr.tr[self.guild.config["lang"]]["errors"]["CommandNotFoundError"].format(command=fonction))

View File

@ -1,8 +1,6 @@
import os
import re
import fs
import traductions as tr

384
bot/modules/survey.py Normal file
View File

@ -0,0 +1,384 @@
import os
import time
import discord
import traductions as tr
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 MainClass:
name = "survey"
def __init__(self, guild):
self.guild = guild
self.current_surveys = {}
self.create_table()
def create_table(self):
with self.guild.bot.database.cursor() as cursor:
create_survey_table_sql = "CREATE TABLE IF NOT EXISTS {guild_id}surveys (" \
" id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY," \
" title VARCHAR(2000) NOT NULL," \
" depart BIGINT," \
" duree BIGINT" \
");".format(guild_id=self.guild.id)
create_choices_table_sql = "CREATE TABLE IF NOT EXISTS {guild_id}survey_choices (" \
" id int(20) NOT NULL AUTO_INCREMENT PRIMARY KEY," \
" survey int(20) NOT NULL," \
" content VARCHAR(1000)," \
" attachment BLOB(67108864)," \
" attachment_name VARCHAR(1000)" \
");".format(guild_id=self.guild.id)
create_vote_table_sql = "CREATE TABLE IF NOT EXISTS {guild_id}survey_votes (" \
" id int NOT NULL AUTO_INCREMENT PRIMARY KEY," \
" choice BIGINT NOT NULL," \
" user_id VARCHAR(20) NOT NULL" \
");".format(guild_id=self.guild.id)
cursor.execute(create_choices_table_sql)
cursor.execute(create_survey_table_sql)
cursor.execute(create_vote_table_sql)
self.guild.bot.database.commit()
async def vote(self, msg, command, args):
try:
await msg.delete()
except discord.Forbidden:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["DiscordForbiddenError"])
if len(args) != 1:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
try:
id_vote = int(args[0])
except ValueError:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotANumberError"])
return
with self.guild.bot.database.cursor() as cursor:
# récupération de l'id du sondage
select_choice_sql = "SELECT survey FROM `{guild_id}survey_choices` WHERE id = %s;".format(
guild_id=self.guild.id)
cursor.execute(select_choice_sql, (id_vote))
survey_id = [r["survey"] for r in cursor.fetchall()]
if len(survey_id) == 0: # Le choix n'existe pas
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["SurveyNotExistsError"])
return
with self.guild.bot.database.cursor() as cursor:
# Récupération de la date de fin du sondage
select_survey_sql = "SELECT depart, duree FROM `{guild_id}surveys` WHERE id=%s;".format(
guild_id=self.guild.id)
cursor.execute(select_survey_sql, (survey_id))
r = cursor.fetchone()
if r["depart"] is not None:
fin = r["depart"] + r["duree"]
else:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotYetPostedError"])
return
# Liste des précédents votes
select_prec_votes_sql = "SELECT choice FROM `{guild_id}survey_votes` WHERE user_id=%s;".format(
guild_id=self.guild.id)
cursor.execute(select_prec_votes_sql, (msg.author.id))
list_votes = [r["choice"] for r in cursor.fetchall()]
# Liste des précédents sondages votés
list_surveys_sql = "SELECT survey FROM `{guild_id}survey_choices` WHERE id=%s".format(
guild_id=self.guild.id)
list_surveys = []
for id_choice in list_votes:
cursor.execute(list_surveys_sql, (id_choice))
list_surveys.append(cursor.fetchone()["survey"])
if fin < time.time(): # Sondage terminé
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["SurveyCompletedError"])
return
if survey_id[0] in list_surveys: # Déjà voté
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["AlreadyVote"])
return
# On peu voter, on insère dans la bdd
with self.guild.bot.database.cursor() as cursor:
sql_insert = "INSERT INTO `{guild_id}survey_votes` (choice, user_id) VALUES (%s, %s);" \
.format(guild_id=self.guild.id)
cursor.execute(sql_insert, (id_vote, msg.author.id))
self.guild.bot.database.commit()
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["survey"]["vote"]
.format(id_auteur=msg.author.id))
async def add_choice(self, msg, command, args):
# L'utilisateur est un administrateur du bot
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
# Vérification du nombre de paramètres
if len(args) < 2 or (len(args) < 1 and len(msg.attachments) < 1):
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
try: # Tentative de conversion en nombre
survey_id = int(args[0])
except ValueError:
msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotANumberError"])
return
# Vérification de l'existance du sondage
with self.guild.bot.database.cursor() as cursor:
sql_id = "SELECT id FROM `{guild_id}surveys`".format(guild_id=self.guild.id)
cursor.execute(sql_id, ())
liste_id = [r["id"] for r in cursor.fetchall()]
if survey_id not in liste_id: # Le sondage n'existe pas
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["SurveyNotExistsError"])
return
# Verification que le sondage n'a pas déjà été publié
with self.guild.bot.database.cursor() as cursor:
sql_depart = "SELECT depart FROM `{guild_id}surveys` WHERE id = %s".format(guild_id=self.guild.id)
cursor.execute(sql_depart, (survey_id))
depart = cursor.fetchone()["depart"]
if depart is not None:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["AlreadySendSurvey"])
return
content = " ".join(args[1:])
# Ecriture du fichier temporaire
with open("temp_attachement" + str(survey_id), "w") as temp_file:
temp_file.write("")
file_name = ""
# Si un fichier est présent dans le message on le sauvegarde
if len(msg.attachments) > 0:
attachment = msg.attachments[0]
if attachment.size > 67108864:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["AttachementTooBigError"])
return
with open("temp_attachement" + str(survey_id), "wb") as temp_file:
await attachment.save(temp_file)
file_name = attachment.filename
# On insère le choix dans la base de données
with self.guild.bot.database.cursor() as cursor:
sql_insert = "INSERT INTO `{guild_id}survey_choices` (survey, content, attachment, attachment_name) VALUES (%s, %s, %s, %s)".format(
guild_id=self.guild.id)
with open("temp_attachement" + str(survey_id), "rb") as temp_file:
cursor.execute(sql_insert, (survey_id, content, temp_file.read(), file_name))
os.remove("temp_attachement" + str(survey_id))
self.guild.bot.database.commit()
async def create_survey(self, msg, command, args):
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
if len(args) < 2:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
else:
date_str = args[0]
content = " ".join(args[1:])
day_split = date_str.split("d")
if len(day_split) == 1 and "d" not in date_str:
jours = "0"
next_split = date_str
elif "d" in date_str and day_split[1] == "":
jours = day_split[0]
next_split = "0h0m0s"
else:
jours = day_split[0]
next_split = day_split[1]
hour_split = next_split.split("h")
if len(hour_split) == 1 and "h" not in date_str:
heures = "0"
next_split = date_str
elif "h" in date_str and hour_split[1] == "":
heures = hour_split[0]
next_split = "0m0s"
else:
heures = hour_split[0]
next_split = hour_split[1]
minute_split = next_split.split("m")
if len(minute_split) == 1 and "h" not in date_str:
minutes = "0"
next_split = date_str
elif "m" in date_str and minute_split[1] == "":
minutes = minute_split[0]
next_split = "0s"
else:
minutes = minute_split[0]
next_split = minute_split[1]
second_split = next_split.split("s")
if len(second_split) == 1 and "s" not in date_str:
secondes = "0"
else:
secondes = second_split[0]
try:
jours = int(jours)
heures = int(heures)
minutes = int(minutes)
secondes = int(secondes)
except ValueError:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotANumberError"])
return
total = jours * 24 * 60 * 60 + heures * 60 * 60 + minutes * 60 + secondes # Durée du sondage
with self.guild.bot.database.cursor() as cursor:
insert_sql = "INSERT INTO `{guild_id}surveys` (title, duree) VALUES (%s, %s);".format(
guild_id=self.guild.id)
cursor.execute(insert_sql, (content, total))
self.guild.bot.database.commit()
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["survey"]["create_survey"]
.format(id=cursor.lastrowid, prefix=self.guild.config["prefix"]))
async def post_survey(self, msg, command, args):
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
if len(args) != 1:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
try:
survey_id = int(args[0])
except ValueError:
msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotANumberError"])
return
# Vérification de l'existance du sondage
with self.guild.bot.database.cursor() as cursor:
sql_id = "SELECT id FROM `{guild_id}surveys`".format(guild_id=self.guild.id)
cursor.execute(sql_id)
liste_id = [r["id"] for r in cursor.fetchall()]
if survey_id not in liste_id:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["SurveyNotExistsError"])
return
# Verification que le sondage n'a pas déjà été publié
with self.guild.bot.database.cursor() as cursor:
sql_depart = "SELECT depart FROM `{guild_id}surveys` WHERE id = %s".format(guild_id=self.guild.id)
cursor.execute(sql_depart, (survey_id))
depart = cursor.fetchone()["depart"]
if depart is not None:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["AlreadySendSurvey"])
return
# Envoi du sondage
with self.guild.bot.database.cursor() as cursor:
sql_update = "UPDATE `{guild_id}surveys` SET depart = %s WHERE id=%s" \
.format(guild_id=self.guild.id)
cursor.execute(sql_update, (int(time.time()), survey_id))
self.guild.bot.database.commit()
with self.guild.bot.database.cursor() as cursor:
sql_choices = "SELECT id from `{guild_id}survey_choices` WHERE survey=%s" \
.format(guild_id=self.guild.id)
cursor.execute(sql_choices, (survey_id))
choices_id = [r["id"] for r in cursor.fetchall()]
with self.guild.bot.database.cursor() as cursor:
sql_survey_title = "SELECT title, duree FROM `{guild_id}surveys` WHERE id = %s" \
.format(guild_id=self.guild.id)
cursor.execute(sql_survey_title, (survey_id))
result = cursor.fetchone()
# Envoi des messages de présentation
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["survey"]["post_survey"]["presentation"]
.format(prefix=self.guild.config["prefix"], heures=int(result["duree"] / 3600)))
await msg.channel.send(result['title'])
# Envoi des message pour chaque choix
for choice_id in choices_id:
with self.guild.bot.database.cursor() as cursor:
sql_choice = "SELECT id,content, attachment, attachment_name FROM `{guild_id}survey_choices` WHERE id=%s" \
.format(guild_id=self.guild.id)
cursor.execute(sql_choice, (choice_id))
result = cursor.fetchone()
if result["attachment_name"]:
with open(result["attachment_name"], "wb") as temp_file:
temp_file.write(result["attachment"])
with open(result["attachment_name"], "rb") as temp_file:
await msg.channel.send("`{prefix}vote {id}` "
.format(prefix=self.guild.config["prefix"], id=result["id"]) + result[
"content"],
file=discord.File(temp_file, filename=str(result["attachment_name"])))
else:
await msg.channel.send(content="`{prefix}vote {id}` "
.format(prefix=self.guild.config["prefix"], id=result["id"]) + result["content"])
async def post_result(self, msg, command, args):
# L'auteur est-t-il un admin?
if msg.author.id not in self.guild.config["master_admins"]:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["PermissionError"])
return
# Nombre de paramètres
if len(args) != 1:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotEnoughParamError"])
return
try:
survey_id = int(args[0])
except ValueError:
msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotANumberError"])
return
# Vérification de l'existance du sondage
with self.guild.bot.database.cursor() as cursor:
sql_id = "SELECT id FROM `{guild_id}surveys`".format(guild_id=self.guild.id)
cursor.execute(sql_id)
liste_id = [r["id"] for r in cursor.fetchall()]
if survey_id not in liste_id:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["SurveyNotExistsError"])
return
# Vérification que le sondage est terminé
with self.guild.bot.database.cursor() as cursor:
# Récupération de la date de fin du sondage
select_survey_sql = "SELECT depart, duree FROM `{guild_id}surveys` WHERE id=%s;".format(
guild_id=self.guild.id)
cursor.execute(select_survey_sql, (survey_id))
r = cursor.fetchone()
if r["depart"] is not None:
fin = r["depart"] + r["duree"]
else:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotYetPostedError"])
return
print(fin, time.time())
if fin > time.time():
await msg.channel.send(tr.tr[self.guild.config["lang"]]["errors"]["NotYetFinishedError"])
return
# Récupération des choix
with self.guild.bot.database.cursor() as cursor:
sql_select_choices = "SELECT id FROM `{guild_id}survey_choices` WHERE survey=%s;".format(guild_id=self.guild.id)
cursor.execute(sql_select_choices, (survey_id))
choices = [r["id"] for r in cursor.fetchall()]
# Récupération des votes
votes = []
for id_choice in choices:
with self.guild.bot.database.cursor() as cursor:
select_votes_sql = "SELECT id FROM `{guild_id}survey_votes` WHERE choice=%s;".format(guild_id=self.guild.id)
cursor.execute(select_votes_sql, (id_choice))
votes.append((id_choice,len(cursor.fetchall())))
votes.sort(key=lambda x: x[1])
total = sum([x[1] for x in votes])
texte = tr.tr[self.guild.config["lang"]]["modules"]["survey"]["result"]["text"]+"```"
i=0
for vote in votes[::-1]:
i+=1
texte += "\n{i} - Choix {id_choix} - {nb_votes} ({pourcentage}%)"\
.format(i=i, id_choix=vote[0], nb_votes=vote[1], pourcentage=vote[1]*100/total)
texte += "```"
await msg.channel.send(texte)
async def on_message(self, msg):
if msg.content.startswith(self.guild.config["prefix"]):
command, *args = msg.content.lstrip(self.guild.config["prefix"]).split(" ")
if command == "vote":
await self.vote(msg, command, args)
elif command == "add_choice":
await self.add_choice(msg, command, args)
elif command == "create_survey":
await self.create_survey(msg, command, args)
elif command == "post_survey":
await self.post_survey(msg, command, args)
elif command == "post_results":
await self.post_result(msg, command, args)
return

View File

@ -28,7 +28,7 @@ tr = {
},
"list_modules": {
"title": "Liste des modules",
},
},
"load": {
},
"unload": {

View File

@ -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": 1048576,
"backupCount": 20,
"encoding": "utf8"
},
"error_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "simple",
"filename": "errors.log",
"maxBytes": 1048576,
"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"
]
}
}

152
main.py
View File

@ -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,20 @@ 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)
foWeb = FoWeb(bot=None, db=db_connection)
def load_config(self):
for guild in self.guilds:
self.guilds_class.update({guild.id: Guild(self, guild.id)})
bot_app = foBot.start(os.environ['FOBOT_DISCORD_TOKEN'], max_messages=100000000)
bot_task = asyncio.ensure_future(bot_app)
def save_config(self):
pass
foWeb.listen(port=8888)
web_task = foWeb.get_task()
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)
myBot = FoBot()
myBot.run(os.environ['DISCORD_TOKEN'], max_messages=100000000)
eventloop.run_forever()

257
web/server.py Normal file
View File

@ -0,0 +1,257 @@
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"))
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", error=None, 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()
)
try:
get_arg = self.get_argument
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"))
except:
self.render("auth/create_user.html", error="Email already exists.", get_arg=get_arg)
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 BotHelpHandler(BaseHandler):
async def get(self):
self.render("bot/help.html")
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),
(r"/bot/help", BotHelpHandler)
]
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())

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

@ -0,0 +1,136 @@
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: #060;
}
#body {
margin: auto;
}
#header {
background-color: #359;
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,74 @@
.documentation {
}
.documentation .summary {
margin:0;
padding: 0;
border: 0.25em solid #3b5998;
position: fixed;
overflow: auto;
width: 25%;
margin:0;
}
.documentation .summary h2 {
background-color: #3b5998;
padding: 0.4em;
}
.documentation .summary ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.documentation .summary ul li {
display: block;
padding: 0;
}
.documentation .summary ul li ul {
display: none;
}
.documentation .summary ul li:hover ul {
display: block;
}
.documentation .summary a {
background-color: #5889ef;
color: black;
display: block;
padding: 0.3em;
text-decoration: none;
}
.documentation .summary a:hover {
background-color: #496ebc;
}
.documentation .summary a:active {
background-color: #4CAF50; /* Add a green color to the "active/current" link */
color: white;
}
.documentation .content {
margin-left: 28%;
}
.documentation .content h2 {
padding: 1em;
}
.documentation .content h3 {
padding: 0.5em;
}
.documentation .content h4 {
padding: 0.25em;
}
.documentation .content ul {
list-style-type: none;
}

View File

@ -0,0 +1,14 @@
{% extends "../base.html" %}
{% block body %}
{% if error %}
<span style="color: red">Error: {{ error }}</span><p>
{% end %}
<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>

108
web/templates/bot/help.html Normal file
View File

@ -0,0 +1,108 @@
{% extends "../base.html" %}
{% block head %}
<link rel="stylesheet" href="{{ static_url("documentation.css") }}" type="text/css">
{% end %}
{% block title %} {{ escape(handler.settings["website_title"]) }} - Help{% end %}
{% block body %}
<div class="documentation">
<div class="summary">
<h2>Summary</h2>
<ul>
<li>
<a href="#module_modules">Module (<code>modules</code>)</a>
<ul>
<li><a href="#">Liste des modules</a></li>
<li><a href="#">Charger un module</a></li>
<li><a href="#">Décharger un module</a></li>
</ul>
</li>
<li>
<a href="#module_help">Help (<code>help</code>)</a>
<ul>
<li><a href="#">Aide générale</a></li>
<li><a href="#">Aide d'un module précit</a></li>
<li><a href="#">Aide d'une commande précise</a></li>
</ul>
</li>
<li><a href="">Cofiguration (<code>config</code>)</a></li>
<li><a href="">Deeptown (<code>deeptown</code>)</a></li>
<li><a href="">Survey (<code>survey</code>)</a></li>
<li><a href="">Pi (<code>pi</code>)</a></li>
<li><a href="">Github (<code>github</code>)</a></li>
<li><a href="">Direct access to database (<code>directAccessDB</code>)</a></li>
</ul>
</div>
<div class="content">
<h2 id="module_modules">Modules (<code>modules</code>)</h2>
<div>
<h3>Liste des modules (<code>list_modules</code>)</h3>
<div>
<p>Commande permettant de lister les modules disponibles. (réservé aux administrateurs)</p>
<div>
<h4>Exemples:</h4>
<ul>
<li><code>list_modules</code>: retourne la liste des modules disponibles</li>
</ul>
</div>
</div>
<h3>Charger un module (<code>load</code>)</h3>
<div>
<p>Commande permettant de charger des modules. (réservé aux administrateurs)</p>
<div>
<h4>Exemples:</h4>
<ul>
<li><code>load help</code>: charge le module <code>help</code></li>
<li><code>load help module1 module2 ...</code>: charger les modules <code>help</code>,
<code>module1</code>,
<code>module2</code>,
...
</li>
</ul>
</div>
</div>
<h3>Décharger un module (<code>unload</code>)</h3>
<div>
<p>Commande permettant de décharger des modules. (réservé aux administrateurs)</p>
<div>
<h4>Exemples:</h4>
<ul>
<li><code>unload module</code>: décharge le module <code>module</code></li>
<li><code>unload module1 module2 module3 ...</code>: décharge les modules <code>module1</code>,
<code>module2</code>,
<code>module3</code>, ...
</li>
</ul>
</div>
</div>
</div>
<h2 id="module_help">Help (<code>help</code>)</h2>
<div>
<h3>Aide générale</h3>
<div>
<p>Commande permettant de lister les différentes commandes actives sur le bot.</p>
<div>
<h4>Exemples:</h4>
<ul>
<li><code>help</code>: Afficher la liste des commandes</li>
</ul>
</div>
</div>
<h3>Aide d'un module précit</h3>
<div>
<p>Commande permmetant d'afficher la description de toutes les commandes d'un module.</p>
<div>
<h4>Exemples:</h4>
<ul>
<li><code>help modules</code>: Affiche la description de toutes les commandes du module
<code>modules</code>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% end %}

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>