Code source de lgrez.features.sync

"""lg-rez / features / Synchronisation GSheets

Récupération et application des données des GSheets : modifications décidées via le Tableau de bord et rôles

"""

import traceback
import time

from discord import Embed
from discord.ext import commands

from lgrez.blocs import tools, bdd, bdd_tools, env, gsheets
from lgrez.blocs.bdd import engine, Tables, Joueurs, Actions, BaseActions, BaseActionsRoles, Roles, Taches
from lgrez.features import gestion_actions


[docs]class TDBModif(): """Modification flag sur le Tableau de bord, à appliquer""" def __init__(self, id, col, val, row, column): """Initializes self.""" #: :class:`int`: ID Discord du joueur concerné self.id = id #: :class:`str`: Colonne de Joueurs à modifier self.col = col #: :class:`object`: Nouvelle valeur self.val = val #: :class:`int`: Numéro de la ligne sur le TDB self.row = row #: :class:`int`: Numéro de la colonne TAMPON sur le TDB self.column = column def __repr__(self): """Returns repr(self)""" return f"<TDBModif id {self.id}: {self.col} = {self.val}>"
[docs]def get_sync(): """Récupère les modifications en attente sur le TDB Charge les données du Tableau de bord (variable d'environment ``LGREZ_TDB_SHEET_ID``), compare les informations qui y figurent avec celles de la base de données (:class:`.bdd.Joueurs`) Returns: :class:`list`\[:class:`.TDBModif`\]: liste des modifications à apporter """ all_cols = bdd_tools.get_cols(Joueurs) cols = [col for col in all_cols if not col.endswith('_')] # On élimine les colonnes locales cols_SQL_types = bdd_tools.get_SQL_types(Joueurs) cols_SQL_nullable = bdd_tools.get_SQL_nullable(Joueurs) primary_col = bdd_tools.get_primary_col(Joueurs) ### RÉCUPÉRATION INFOS GSHEET SHEET_ID = env.load("LGREZ_TDB_SHEET_ID") workbook = gsheets.connect(SHEET_ID) # Tableau de bord sheet = workbook.worksheet("Journée en cours") values = sheet.get_all_values() # Liste de liste des valeurs des cellules head = values[2] # Ligne d'en-têtes (noms des colonnes) = 3e ligne du TDB TDB_index = {col: head.index(col) for col in cols} # Dictionnaire des indices des colonnes GSheet pour chaque colonne de la table TDB_tampon_index = {col: head.index(f"tampon_{col}") for col in cols if col != 'discord_id'} # Idem pour la partie « tampon » # CONVERSION INFOS GSHEET EN UTILISATEURS joueurs_TDB = [] # Liste des joueurs tels qu'actuellement dans le TDB ids_TDB = [] # discord_ids des différents joueurs du TDB rows_TDB = {} # Indices des lignes ou sont les différents joueurs du TDB for i_row in range(len(values)): L = values[i_row] # On parcourt les lignes du TDB id_cell = L[TDB_index[primary_col]] if id_cell.isdigit(): # Si la cellule contient bien un ID (que des chiffres, et pas vide) id = int(id_cell) joueur_TDB = {col: bdd_tools.transtype(L[TDB_index[col]], col, cols_SQL_types[col], cols_SQL_nullable[col]) for col in cols} # Dictionnaire correspondant à l'utilisateur joueurs_TDB.append(joueur_TDB) ids_TDB.append(id) rows_TDB[id] = i_row ### RÉCUPÉRATION UTILISATEURS CACHE joueurs_BDD = Joueurs.query.all() # Liste des joueurs tels qu'actuellement en cache ids_BDD = [joueur_BDD.discord_id for joueur_BDD in joueurs_BDD] ### COMPARAISON modifs = [] # modifs à porter au TDB (liste de TDBModifs) for joueur_BDD in joueurs_BDD.copy(): # Joueurs dans le cache supprimés du TDB if joueur_BDD.discord_id not in ids_TDB: joueurs_BDD.remove(joueur_BDD) bdd.session.delete(joueur_BDD) for joueur_TDB in joueurs_TDB: # Différences id = joueur_TDB["discord_id"] if id not in ids_BDD: # Si joueur dans le cache pas dans le TDB raise ValueError(f"Joueur {joueur_TDB['nom']} hors BDD : vérifier processus d'inscription") joueur_BDD = [joueur for joueur in joueurs_BDD if joueur.discord_id == id][0] # joueur correspondant dans le cache for col in cols: if getattr(joueur_BDD, col) != joueur_TDB[col]: # Si <col> diffère entre TDB et cache modif = TDBModif(id, col, joueur_TDB[col], rows_TDB[id], TDB_tampon_index[col]) modifs.append(modif) # On ajoute les modifs ### RETOURNAGE DES RÉSULTATS return modifs
[docs]def validate_sync(modifs): """Valide des modificatons sur le Tableau de bord (case plus en rouge) Args: modifs (:class:`list`\[:class:`.TDBModif`\]): liste des modifications à apporter Modifie sur le Tableau de bord (variable d'environment ``LGREZ_TDB_SHEET_ID``) et applique les modifications contenues dans modifs """ ### APPLICATION DES MODIFICATIONS SUR LE TDB SHEET_ID = env.load("LGREZ_TDB_SHEET_ID") workbook = gsheets.connect(SHEET_ID) # Tableau de bord sheet = workbook.worksheet("Journée en cours") modifs_lc = [(modif.row, modif.column, modif.val) for modif in modifs] # On transforme les infos en coordonnées dans le TDB : ID -> ligne et col -> colonne, gsheets.update(sheet, modifs_lc)
[docs]async def modif_joueur(ctx, joueur_id, modifs, silent=False): """Attribue les modifications demandées au joueur Args: ctx (:class:`~discord.ext.commands.Context`): contexte quelconque du bot joueur_id (:class:`int`): id Discord du joueur concerné modifs (:class:`list`\[:class:`.TDBModif`\]): liste des modifications à apporter silent (:class:`bool`): si ``True``, no notifie pas le joueur des modifications Pour chaque modifications de ``modif``, applique les conséquences adéquates (rôles, nouvelles actions, tâches planifiées...) et informe le joueur si ``silent`` vaut ``False``. """ joueur = Joueurs.query.get(int(joueur_id)) assert joueur, f"!sync : joueur d'ID {joueur_id} introuvable" member = ctx.guild.get_member(joueur.discord_id) assert member, f"!sync : member {joueur} introuvable" chan = ctx.guild.get_channel(joueur.chan_id_) assert chan, f"!sync : chan privé de {member} introuvable" changelog = f"\n- {member.display_name} (@{member.name}#{member.discriminator}) :\n" notif = "" for modif in modifs: changelog += f" - {modif.col} : {modif.val}\n" if modif.col == "nom": # Renommage joueur await chan.edit(name=f"conv-bot-{modif.val}") await member.edit(nick=modif.val) if not silent: notif += f":arrow_forward: Tu t'appelles maintenant {tools.bold(modif.val)}.\n" elif modif.col == "chambre" and not silent: # Modification chambre notif += f":arrow_forward: Tu habites maintenant en chambre {tools.bold(modif.val)}.\n" elif modif.col == "statut": if modif.val == "vivant": # Statut = vivant await member.add_roles(tools.role(ctx, "Joueur en vie")) await member.remove_roles(tools.role(ctx, "Joueur mort")) if not silent: notif += f":arrow_forward: Tu es maintenant en vie. EN VIE !!!\n" elif modif.val == "mort": # Statut = mort await member.add_roles(tools.role(ctx, "Joueur mort")) await member.remove_roles(tools.role(ctx, "Joueur en vie")) if not silent: notif += f":arrow_forward: Tu es malheureusement décédé(e) :cry:\nÇa arrive même aux meilleurs, en espérant que ta mort ait été belle !\n" # Actions à la mort for action in Actions.query.filter_by(player_id=joueur.discord_id, trigger_debut="mort"): await gestion_actions.open_action(ctx, action, chan) elif modif.val == "MV": # Statut = MV await member.add_roles(tools.role(ctx, "Joueur en vie")) await member.remove_roles(tools.role(ctx, "Joueur mort")) if not silent: notif += f":arrow_forward: Te voilà maintenant réduit(e) au statut de mort-vivant... Un MJ viendra te voir très vite, si ce n'est déjà fait, mais retient que la partie n'est pas finie pour toi !\n" elif not silent: # Statut = autre notif += f":arrow_forward: Nouveau statut : {tools.bold(modif.val)} !\n" elif modif.col == "role": # Modification rôle old_bars = BaseActionsRoles.query.filter_by(role=joueur.role).all() old_actions = [] for bar in old_bars: old_actions.extend(Actions.query.filter_by(action=bar.action, player_id=joueur.discord_id).all()) for action in old_actions: gestion_actions.delete_action(ctx, action) # On supprime les anciennes actions de rôle (et les tâches si il y en a) new_bars = BaseActionsRoles.query.filter_by(role=modif.val).all() # Actions associées au nouveau rôle new_bas = [BaseActions.query.get(bar.action) for bar in new_bars] # Nouvelles BaseActions cols = [col for col in bdd_tools.get_cols(BaseActions) if not col.startswith("base")] new_actions = [Actions(player_id=joueur.discord_id, **{col: getattr(ba, col) for col in cols}, cooldown=0, charges=ba.base_charges) for ba in new_bas] await tools.log(ctx, str(new_actions)) for action in new_actions: gestion_actions.add_action(ctx, action) # Ajout et création des tâches si trigger temporel role = tools.nom_role(modif.val) if not role: # role <modif.val> pas en base : Error! role = f{modif.val} »" await tools.log(ctx, f"{tools.mention_MJ(ctx)} ALED : rôle \"{modif.val}\" attribué à {joueur.nom} inconnu en base !") if not silent: notif += f":arrow_forward: Ton nouveau rôle, si tu l'acceptes : {tools.bold(role)} !\nQue ce soit pour un jour ou pour le reste de la partie, renseigne toi en tapant {tools.code(f'!roles {modif.val}')}.\n" elif modif.col == "camp" and not silent: # Modification camp notif += f":arrow_forward: Tu fais maintenant partie du camp « {tools.bold(modif.val)} ».\n" elif modif.col == "votant_village" and not silent: if modif.val: # votant_village = True notif += f":arrow_forward: Tu peux maintenant participer aux votes du village !\n" else: # votant_village = False notif += f":arrow_forward: Tu ne peux maintenant plus participer aux votes du village.\n" elif modif.col == "votant_loups" and not silent: if modif.val: # votant_loups = True notif += f":arrow_forward: Tu peux maintenant participer aux votes des loups ! Amuse-toi bien :wolf:\n" else: # votant_loups = False notif += f":arrow_forward: Tu ne peux maintenant plus participer aux votes des loups.\n" elif modif.col == "role_actif" and not silent: if modif.val: # role_actif = True notif += f":arrow_forward: Tu peux maintenant utiliser tes pouvoirs !\n" else: # role_actif = False notif += f":arrow_forward: Tu ne peux maintenant plus utiliser aucun pouvoir.\n" bdd_tools.modif(joueur, modif.col, modif.val) # Dans tous les cas, on modifie en base (après, pour pouvoir accéder aux vieux attribus plus haut) if not silent: await chan.send(f":zap: {member.mention} Une action divine vient de modifier ton existence ! :zap:\n" + f"\n{notif}\n" + tools.ital(":warning: Si tu penses qu'il y a erreur, appelle un MJ au plus vite !")) return changelog
[docs]class Sync(commands.Cog): """Sync - Commandes de synchronisation des GSheets vers la BDD et les joueurs""" @commands.command() @tools.mjs_only async def sync(self, ctx, silent=False): """Récupère et applique les modifications du Tableau de bord (COMMANDE MJ) Args: silent: si spécifié (quelque soit sa valeur), les joueurs ne sont pas notifiés des modifications. Cette commande va récupérer les modifications en attente sur le Tableau de bord (lignes en rouge), modifer la BDD Joueurs, et appliquer les modificatons dans Discord le cas échéant : renommage des utilisateurs, modification des rôles... """ await ctx.send("Récupération des modifications...") async with ctx.typing(): modifs = get_sync() # Récupération du dictionnaire {joueur_id: modified_attrs} silent = bool(silent) changelog = f"Synchronisation TDB (silencieux = {silent}) :" if not modifs: await ctx.send("Pas de nouvelles modificatons.") return dic = {} for modif in modifs: if modif.id not in dic: dic[modif.id] = [] dic[modif.id].append(modif) message = await ctx.send(f"{len(modifs)} modification(s) trouvée(s) pour {len(dic)} joueur(s), go ?") if not await tools.yes_no(ctx.bot, message): await ctx.send("Mission aborted.") return # Go sync done = [] async with ctx.typing(): for joueur_id, modifs in dic.items(): # Joueurs dont au moins un attribut a été modifié try: changelog += await modif_joueur(ctx, joueur_id, modifs, silent) except Exception: changelog += traceback.format_exc() await ctx.send(f"Erreur joueur {joueur_id}, passage au suivant, voir logs pour les détails") else: done.extend(modifs) bdd.session.commit() if done: validate_sync(done) await tools.log(ctx, changelog, code=True) await ctx.send(f"Fait (voir {tools.channel(ctx, 'logs').mention} pour le détail)") @commands.command() @tools.mjs_only async def fillroles(self, ctx): """Remplit les tables des rôles / actions et #roles depuis le GSheet ad hoc (COMMANDE MJ) - Remplit les tables :class:`.bdd.Roles`, :class:`.bdd.BaseActions` et :class:`.bdd.BaseActionsRoles` avec les informations du Google Sheets "Rôles et actions" (variable d'environnement ``LGREZ_ROLES_SHEET_ID``) ; - Vide le chan ``#roles`` puis le remplit avec les descriptifs de chaque rôle. Utile à chaque début de saison / changement dans les rôles/actions. Écrase toutes les entrées déjà en base, mais ne supprime pas celles obsolètes. """ SHEET_ID = env.load("LGREZ_ROLES_SHEET_ID") workbook = gsheets.connect(SHEET_ID) # Tableau de bord for table_name in ["Roles", "BaseActions", "BaseActionsRoles"]: await ctx.send(f"Remplissage de la table {tools.code(table_name)}...") async with ctx.typing(): sheet = workbook.worksheet(table_name) values = sheet.get_all_values() # Liste de liste des valeurs des cellules table = Tables[table_name] cols = bdd_tools.get_cols(table) SQL_types = bdd_tools.get_SQL_types(table) SQL_nullable = bdd_tools.get_SQL_nullable(table) primary_col = bdd_tools.get_primary_col(table) cols_index = {col: values[0].index(col) for col in cols} # Dictionnaire des indices des colonnes GSheet pour chaque colonne de la table existants = {getattr(item, primary_col):item for item in table.query.all()} for L in values[1:]: args = {col: bdd_tools.transtype(L[cols_index[col]], col, SQL_types[col], SQL_nullable[col]) for col in cols} id = args[primary_col] if id in existants: for col in cols: if getattr(existants[id], col) != args[col]: bdd_tools.modif(existants[id], col, args[col]) else: bdd.session.add(table(**args)) bdd.session.commit() await ctx.send(f"Table {tools.code(table_name)} remplie !") await tools.log(ctx, f"Table {tools.code(table_name)} remplie !") chan_roles = tools.channel(ctx, "rôles") await ctx.send(f"Vidage de {chan_roles.mention}...") async with ctx.typing(): await chan_roles.purge(limit=1000) roles = {camp: Roles.query.filter_by(camp=camp).all() for camp in ["village", "loups", "nécro", "solitaire", "autre"]} await ctx.send(f"Remplissage... (temps estimé : {sum([len(v) + 2 for v in roles.values()]) + 1} secondes)") t0 = time.time() await chan_roles.send(f"Voici la liste des rôles : (accessible en faisant {tools.code('!roles')}, mais on l'a mis là parce que pourquoi pas)\n\n——————————————————————————") async with ctx.typing(): for camp, roles_camp in roles.items(): if roles_camp: await chan_roles.send(embed=Embed(title=f"Camp : {camp}").set_image(url=tools.emoji_camp(ctx, camp).url)) await chan_roles.send(f"——————————————————————————") for role in roles_camp: await chan_roles.send(f"{tools.emoji_camp(ctx, role.camp)} {tools.bold(role.prefixe + role.nom)}{role.description_courte} (camp : {role.camp})\n\n{role.description_longue}\n\n——————————————————————————") await ctx.send(f"{chan_roles.mention} rempli ! (en {(time.time() - t0):.4} secondes)")