Compare commits

...

2 Commits

Author SHA1 Message Date
2ea4c30212 First avalon commit 2018-10-19 20:21:25 +02:00
13709a2059 First commit for web control 2018-10-08 23:41:54 +02:00
25 changed files with 1035 additions and 211 deletions

View File

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

70
Pipfile.lock generated
View File

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

152
bot/fobot.py Normal file
View File

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

134
bot/modules/avalon.py Normal file
View File

@ -0,0 +1,134 @@
import random
from bot import traductions as tr
import json
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 = "avalon"
def __init__(self, guild):
self.guild = guild
# Init database
self.curent_games = []
self.current_waiting_players = []
self.current_roles = []
self.current_players = []
with self.guild.bot.database.cursor() as cursor:
sql_init = "CREATE TABLE IF NOT EXISTS {guild_id}avalon (" \
" id int(5) NOT NULL AUTO_INCREMENT PRIMARY KEY," \
" nb_joueurs int(5) NOT NULL," \
" gentil_a varchar(50) NOT NULL," \
" gentil_b varchar(50) NOT NULL," \
" gentil_c varchar(50)," \
" gentil_d varchar(50)," \
" gentil_e varchar(50)," \
" gentil_f varchar(50)," \
" mechant_a varchar(50) NOT NULL," \
" mechant_b varchar(50)," \
" mechant_c varchar(50)," \
" mechant_d varchar(50)," \
" merlin varchar(50) NOT NULL," \
" assassin varchar(50) NOT NULL," \
" mordred varchar(50)," \
" perceval varchar(50)," \
" morgane varchar(50)," \
" oberon varchar(50)," \
" vainqueur varchar(50)" \
")".format(guild_id=self.guild.id)
cursor.execute(sql_init)
async def start(self, msg, command, args):
if len(self.current_waiting_players) >= 5:
await msg.channel.send(
tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonstart"]["notenoughtplayers"])
return
elif len(self.current_waiting_players) != len(self.current_roles):
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonstart"]["rolesnotmatch"])
return
else:
self.current_games.append({
"channel": msg.channel.id,
"players": self.current_waiting_players,
"gentils": [],
"mechants": [],
"merlin": None,
"assassin": None,
"mordred": None,
"perceval": None,
"morgane": None,
"oberon": None
})
for player in self.current_waiting_players:
role = random.choose(self.current_roles)
self.current_roles.remove()
self.current_waiting_players = []
async def quit(self, msg, command, args):
if msg.author.id in self.current_waiting_players:
self.current_waiting_players.remove(msg.author.id)
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonquit"]["quit"]
.format(player_id=msg.author.id,
nb_players=len(self.current_waiting_players)))
elif msg.author.id in self.current_players:
self.quitting_players.append(msg.author.id)
to_del = []
# Verify if everyone want to quit game
for game in self.curent_games:
stop_game = True
for game_player in game.players:
if game_player not in self.quitting_player:
stop_game = False
if stop_game:
to_del.append(game)
for game in to_del:
self.curent_games.remove(game)
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonquit"]["alreadyplaying"]
.format(player_id=msg.authpr.id))
async def join(self, msg, command, args):
# Personne pas déjà en train d'attendre, ni en train de jouer
if msg.author.id not in self.current_waiting_players + self.current_players:
self.current_waiting_players.append(msg.author.id)
await msg.channel.send(
tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonjoin"]["join"]
.format(player_id=msg.author.id,
nb_players=len(self.current_waiting_players)))
if len(self.current_waiting_players) >= 5:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonjoin"]["canplay"]
.format(prefix=self.guild.config["prefix"]))
elif msg.author.id in self.current_players:
await msg.channel.send(
tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonjoin"]["alreadyplaying"]
.format(player_id=msg.author.id))
elif msg.author.id in self.current_waiting_players:
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonjoin"]["alreadywaiting"]
.format(player_id=msg.author.id))
async def stats(self, msg, command, args):
with self.guild.bot.database.cursor() as cursor:
cursor.execute("SELECT id,nb_joueurs,vainqueur FROM {guild_id}avalon;".format(guild_id=self.guild.id))
results = cursor.fetchall()
nb_games = len(results)
nb_victoire_gentil = len(list([result for result in results if results["vainqueur"] == "gentil"]))
nb_victoire_mechant = nb_games - nb_victoire_gentil
await msg.channel.send(tr.tr[self.guild.config["lang"]]["modules"]["avalon"]["avalonstats"]
.format(nb_games=nb_games,
nb_victoire_gentil=nb_victoire_gentil,
nb_victoire_mechant=nb_victoire_mechant))
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 == "avalonstats":
await self.stats(msg, command, args)
elif command == "avalonjoin":
await self.join(msg, command, args)
elif command == "avalonquit":
await self.quit(msg, command, args)
return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -115,7 +115,7 @@ tr = {
("`(prefix}pi`", "Affiche les 2000 premières décimales de pi."), ("`(prefix}pi`", "Affiche les 2000 premières décimales de pi."),
("`{prefix}pi 2000`", "Affiche 2000 décimales de pi à partir de la 2000ème"), ("`{prefix}pi 2000`", "Affiche 2000 décimales de pi à partir de la 2000ème"),
], ],
},"fpi": { }, "fpi": {
"description": "Recherche l'expression régulière dans pi", "description": "Recherche l'expression régulière dans pi",
"examples": [ "examples": [
("`{prefix}fpi 12345`", "Affiche les 10 premières occurences de 12345 dans pi"), ("`{prefix}fpi 12345`", "Affiche les 10 premières occurences de 12345 dans pi"),
@ -126,18 +126,75 @@ tr = {
"pi": "Voici les 2000 décimales de pi que vous avez demandé (à partir de la {debut}ème):", "pi": "Voici les 2000 décimales de pi que vous avez demandé (à partir de la {debut}ème):",
"fpi": "Une occurence de votre recherche a été trouvée à la {debut}ème place: `{before}`{find}`{after}`", "fpi": "Une occurence de votre recherche a été trouvée à la {debut}ème place: `{before}`{find}`{after}`",
}, },
"avalon": {
"description": "Commandes relatives au jeu avalon",
"help": {
"avalonstats": {
"description": "Donne les stats du jeu avalon sur le serveur",
"examples": [
("`{prefix}`avalonstats", "Affiche les stats des parties avalon du serveur"),
],
},
"avalonjoin": {
"description": "Rejoindre la liste d'attente des joueurs pour avalon",
"examples": [
("`{prefix}avalonjoin`", "Rejoindre la liste d'attente d'une partie avalon"),
],
},
"avalonstart": {
"description": "Lancer la partie d'avalon",
"examples": [
("`{prefix}`avalonstart", "Lance la partie d'avalon si il ya a assez de joueurs.")
]
}
},
"avalonstats": "Depuis la création du jeu sur ce serveur {nb_games} parties de avalon ont étés jouées, "
"les gentils ont gagnés {nb_victoire_gentil} et les méchants ont gagnés "
"{nb_victoire_mechant}",
"avalonjoin": {
"join": "<@{player_id}>, vous avec rejoint la partie avalon, il y a actuellement {nb_players} "
"joueurs dans cette partie",
"canplay": "Il y a maintenant assez de joueurs, tapez `{prefix}avalonstart` pour démarer la partie.",
"alreadyplay": "Vous ne pouvez pas jouer à deux parties en même temps.",
"alreadywaiting": "Vous attendez déja de pouvoir jouer",
},
"avalonquit": {
"quit": "<@{player_id>, vous avez bien quitté la partie, il reste {nb_players} joueurs",
"alreadyplaying": "<@{player_id}> vous êtes dans une partie déjà commencé, tous les joueurs de la "
"partie doivent quitter pour que la partie se termine."
},
"avalonstart": {
"start": "La partie est lancée, avec {nb_joueurs}. Les roles {liste_roles} sont présents. Bonne "
"partie!",
"roles": {
"gentil": "gentil",
"mechant": "mechant",
"merlin": "merlin",
"assassin": "assassin",
"mordred": "mordred",
"perceval": "perceval",
"morgane": "morgane",
"oberon": "obéron"
},
"bienvenue": "Je vous souhaite à tous la bienvenue autour de cette table ronde, maleuresement au moins "
"deux méchants sont présents. Leur but est de faire échouer trois quêtes du roi arthur, ou "
"de refuser cinq écuipes de quêtes d'affilé. Le roi arthur va proposer cinq quêtes, vous "
"devrez pour chacunes composer un équipe, la faire valider par les autres et réussir la "
"quête."
},
},
"github": { "github": {
"description": "Commands relatives à discord", "description": "Commands relatives à discord",
"help": { "help": {
"sourcecode": { "sourcecode": {
"description": "Donne un lien vers mon code source (il est là comme ca tu a pas retapper la \ "description": "Donne un lien vers mon code source (il est là comme ca tu a pas retapper la \
commande :smile: https://github.com/Fomys/foBot", commande :smile: https://github.com/Fomys/foBot",
"examples":[ "examples": [
("`prefix`sourcecode", "Affiche mon code source") ("`{prefix}`sourcecode", "Affiche mon code source")
] ]
}, },
}, },
"sourcecode":"Mon code source est disponible sur github: https://github.com/Fomys/foBot", "sourcecode": "Mon code source est disponible sur github: https://github.com/Fomys/foBot",
}, },
"tools": { "tools": {
"description": "Commandes utiles", "description": "Commandes utiles",

View File

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

151
main.py
View File

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

249
web/server.py Normal file
View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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