Discord Bot avec fonction d'enregistrement commençant par Python: (3) Coopération avec la base de données

introduction

Cet article est une continuation du précédent Discord Bot avec fonction d'enregistrement commençant par Python: (2) Fonctions pratiques (extension de Bot, Cog, Embed).

Dans cet article, nous travaillerons avec la base de données qui sera nécessaire à mesure que le Bot se développera. Implémentez la fonction $ prefix qui change le préfixe pour chaque serveur utilisant la base de données.

Nous prévoyons d'écrire un total de 7 fois et avons fini d'écrire jusqu'à 5 articles.

  1. Discord Bot avec fonction d'enregistrement commençant par Python: (1) Mise en route discord.py
  2. Discord Bot avec fonction d'enregistrement commençant par Python: (2) Fonction pratique (extension Bot, Cog, Embed)
  3. Discord Bot avec fonction d'enregistrement commençant par Python: (3) Coopération avec la base de données
  4. Discord Bot avec fonction d'enregistrement commençant par Python: (4) Lire des fichiers musicaux
  5. Discord Bot avec fonction d'enregistrement commençant par Python: (5) Exploitez directement l'API Discord

Utiliser des variables d'environnement

Jusqu'à présent, le jeton Bot a été bloqué dans le code source, mais c'est extrêmement gênant lorsque vous souhaitez le partager avec un tiers sur GitHub, etc. Utilisez donc la fonction de Docker Compose pour les collecter en tant que variables d'environnement.

Tout d'abord, créez un fichier appelé .env dans la racine du projet et enregistrez-y les variables d'environnement.

sh:./.env


BOT_TOKEN=NDIAHJffoaj.adwdeg....

Ici, BOT_TOKEN = token est défini. Modifiez docker-compose.dev.yml pour rendre les variables d'environnement enregistrées de cette manière disponibles sur le conteneur Docker.

yml:./docker-compose.dev.yml


version: "3.8"
services: 
  dbot:
    build:
      context: ./src
      dockerfile: dev.dockerfile
    tty: true
    working_dir: /bot/app
    entrypoint: bash ./entrypoint.dev.sh
    env_file: #Avec cette ligne
      - .env  #Cette ligne
    volumes:
      - ./src:/bot

La variable d'environnement créée en passant le chemin du fichier précédent à ʻenv_file` est passée au conteneur.

Ensuite, modifiez la partie jeton de __main __. Py qui a été frappée directement jusqu'à présent comme suit.

python:./src/app/dbot/__main__.py


from dbot.core.bot import DBot
import os
DBot(os.environ["BOT_TOKEN"]).run()

En rassemblant des informations telles que des variables d'environnement que vous ne voulez pas que l'on connaisse, vous pouvez atteindre l'objectif en téléchargeant uniquement ce fichier vers un tiers dans un format privé. Par exemple, si vous ne voulez pas pousser ce .env vers GitHub, créez un nouveau fichier appelé .gitignore et ajoutez .env pour qu'il ne soit pas surveillé par Git et ne soit pas poussé à distance.

.gitignore


.env

Si vous redémarrez le conteneur et qu'il peut être démarré normalement, il réussit. Veuillez noter que si vous modifiez le fichier de variable d'environnement, il sera reflété après le redémarrage du conteneur.

Utilisons la base de données

Au fur et à mesure que Bot devient plus complexe, vous souhaiterez peut-être stocker des données sur chaque serveur afin de pouvoir l'utiliser. Pour réaliser cela, par exemple, un fichier CSV peut être préparé et écrit directement, mais il existe divers problèmes étant donné que les demandes des utilisateurs arrivent de manière asynchrone. Alors cette fois, essayons de sauvegarder les données en utilisant la base de données.

Créer une base de données

MySQL est utilisé ici, mais tout est OK tant que vous avez votre moteur de base de données préféré. Configurons le service MySQL en tant que conteneur Docker. Modifiez docker-compose.dev.yml comme suit. L'écriture suivante est basée sur cet article .. Ceux qui ont créé .gitignore devraient exclure / db.

yml:./docker-compose.dev.yml


version: "3.8"
services: 
  dbot:
    #Abréviation
  mysql:
    image: mysql:8.0
    restart: always
    env_file: 
      - .env
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/sql:/docker-entrypoint-initdb.d

Comme mentionné dans l'article précédent, le conteneur MySQL vous demande d'entrer le nom de la base de données à créer en premier et le mot de passe de l'utilisateur dans les variables d'environnement, alors mettez-les ensemble dans .env.

sh:./.env


BOT_TOKEN=...
MYSQL_ROOT_PASSWORD=supersecret
MYSQL_USER=docker
MYSQL_DATABASE=discord
MYSQL_PASSWORD=veryverysecret

Après avoir créé jusqu'à ce point, tapez ./run.sh dev down et ./run.sh dev up -d mysql pour démarrer uniquement la base de données.

Vous devez manipuler SQL pour faire fonctionner la base de données, mais il est plus facile de définir uniquement le schéma de la base de données autant que possible et d'effectuer le travail de migration automatiquement.

Pour utiliser ORM et la migration ...

Par conséquent, cette fois, nous utiliserons SQLAlchemy, qui est un mappage de relation d'objet (** ORM **) de SQL. Et, pour exécuter SQLAlchemy, un client qui exploite la base de données est requis, mais ici nous utilisons aiomysql, qui est un client qui répond à l'exigence de ** asynchrone et non bloquant **.

Utilisez ensuite Alembic écrit en Python comme outil de migration de base de données. Tout d'abord, installez ces trois.

À . / Src / app

$ pipenv install sqlalchemy aiomysql
$ pipenv install alembic --dev

Est exécuté.

Après l'installation, créez le dossier . / Src / app / dbot / models et créez les fichiers suivants.

Modifiez model.py comme suit.

from sqlalchemy import MetaData, Table, Column, BigInteger, String


meta = MetaData()

guild = Table(
    "guild",
    meta,
    Column("id", BigInteger(), nullable=False, primary_key=True),
    Column("prefix", String(8), server_default="$", nullable=False)
)

Bien qu'il s'agisse d'une grammaire propre à SQLAlchemy, une table est définie en combinant Table et Column. Le nom de la table est écrit dans le premier argument de Table et les informations de colonne de la table sont écrites après le troisième argument, mais quelle est l'identité de meta dans le deuxième argument est une variable qui stocke toutes les informations de définition de la base de données. Cela correspond. En transmettant cette méta à l'extérieur, vous pouvez utiliser les informations de la base de données créée par SQL Alchemy.

La table créée ici est une table pour changer le préfixe («$») pour chaque serveur.

Alembic va migrer la base de données en fonction de cette méta. En d'autres termes, lorsque vous souhaitez créer une nouvelle table, le développeur n'a pas à créer la table directement à l'aide de SQL et peut se concentrer sur la définition du schéma.

Pour utiliser Alembic, vous devez taper la commande ʻalembic initpour effectuer les réglages initiaux. Si vous créez un dossier. / Src / app / alembic et exécutez ʻalembic init . dans ce dossier, divers fichiers seront générés.

Les fichiers à éditer sont ʻenv.py et ʻalembic.ini. ʻAlembic.ini est spécifié au format mysql + pymysql: // nom d'utilisateur: mot de passe @ nom du conteneur de base de données / nom de la base de données` comme indiqué ci-dessous.

ini:./src/app/alembic/alembic.ini


# A generic, single database configuration.

[alembic]

#Abréviation

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = mysql+pymysql://docker:veryverysecret@mysql/discord

#Abréviation

ʻEnv.py` doit importer la méta de plus tôt, mais comme le chemin vers dbot se trouve dans le répertoire parent, modifiez-le comme suit

python:./src/app/alembic/env.py


from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

##Ajoutez ce qui suit##

import sys
import os

sys.path.append(os.pardir)

from dbot.models.model import meta

##Jusque là##

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

#Abréviation

##Changer cette valeur
target_metadata = meta

#Abréviation

Ensuite, si vous exécutez ʻalembic revision -m "Init" sur le dossier ʻalembic, le fichier de schéma généré à la suite de la lecture de meta sera créé dans le dossier versions.

Vous pouvez migrer ceci vers une base de données existante, mais la commande pour cela est ʻalembic upgrade head. Editez ʻentrypoint.dev.sh pour exécuter ces commandes.

sh:./src/app/entrypoint.dev.sh


set -eu
cd alembic
alembic upgrade head
alembic revision --autogenerate
alembic upgrade head
cd ..
nodemon --signal SIGINT -e py,ini --exec python -m dbot

Enfin, installez ʻalembic sur la dernière ligne de dev.dockerfile`.

dockerfile:./src/dev.dockerfile


#Omission
RUN pip install alembic

Maintenant, vous êtes prêt à partir. Chaque fois que vous démarrez le conteneur Docker, la migration sera exécutée.

Maintenant que nous l'avons rendu automatiquement sensible aux changements de schéma, mettons-les à la disposition de Bot.

Pontage entre DBot et MySQL

Créez . / Src / app / dbot / db.py pour définir la classe à connecter à la base de données.

python:./src/app/dbot/db.py


import os
import asyncio
from aiomysql.sa import create_engine


class DB:
    async def __aenter__(self, loop=None):
        if loop is None:
            loop = asyncio.get_event_loop()
        engine = await create_engine(
            user=os.environ["MYSQL_USER"],
            db=os.environ["MYSQL_DATABASE"],
            host="mysql",
            password=os.environ["MYSQL_PASSWORD"],
            charset="utf8",
            autocommit=True,
            loop=loop
        )
        self._connection = await engine.acquire()
        return self

    async def __aexit__(self, *args, **kwargs):
        await self._connection.close()

    async def execute(self, query, *args, **kwargs):
        return await self._connection.execute(query, *args, **kwargs)

L'implémentation ci-dessus est [cet article](https://qiita.com/halhorn/items/eb2951a024ae255e6a21#aiomysql-%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3 % E3% 82% 92% E4% BD% BF% E3% 81% A3% E3% 81% A6% E5% AE% 9F% E9% 9A% 9B% E3% 81% AB% E5% AE% 9F% E8 % A1% 8C% E3% 81% 99% E3% 82% 8B) est utilisé comme référence. Des collouts inconnus tels que «aenter» sont utilisés avec «with». aenter signifie ʻa (= async) + ʻenter, donc pour obtenir une connexion à cette base de données

async with DB() as db:
    db.execute("Requete")

Vous pouvez l'utiliser en faisant comme.

Ne touchez pas à SQL, etc.

Enfin, je ne veux pas écrire autant que possible SQL, peu importe la quantité d'ORM. Par conséquent, créez une classe qui peut CRUD (créer / acquérir / mettre à jour / supprimer) des données pour chaque table. Créez . / Src / app / dbot / models / guild.py et modifiez-le comme suit.

python:./src/app/dbot/models/guild.py


from dbot.models import model
from dbot.db import DB


class CRUDBase:
    @staticmethod
    async def execute(query, *args, **kwargs):
        async with DB() as db:
            result = await db.execute(query, *args, **kwargs)
        return result


class Guild(CRUDBase):
    def __init__(self, guild_id):
        self.guild_id = guild_id

    async def get(self):
        q = model.guild.select().where(self.guild_id == model.guild.c.id)
        result = await self.execute(q)
        return await result.fetchone()

    async def set(self, **kwargs):
        q = model.guild.update(None).where(
            self.guild_id == model.guild.c.id
        ).values(**kwargs)
        await self.execute(q)
        return self

    async def delete(self):
        q = model.guild.delete(None).where(self.guild_id == model.guild.c.id)
        await self.execute(q)
        return self

    @classmethod
    async def create(cls, guild_id):
        q = model.guild.insert(None).values(id=guild_id)
        guild = cls(guild_id)
        await cls.execute(q)
        return guild

    @staticmethod
    async def get_all(cls):
        q = model.guild.select()
        results = await cls.execute(q)
        return await results.fetchall()

L'explication de la façon d'écrire une requête dans SQLAlchemy sort du cadre de cet article, je vais donc l'omettre, mais vous pouvez écrire une requête avec une syntaxe similaire à SQL.

Vous pouvez maintenant récupérer des informations de la base de données en faisant ʻawait Guild (guild.id) .get () `sans vous soucier de SQL à chaque fois pendant le développement.

Implémentation de la commande $ prefix

Pour modifier à nouveau le préfixe, suivez les étapes ci-dessous.

  1. [Principale prémisse] Modifiez les spécifications afin que le préfixe puisse être modifié pour chaque serveur.
  2. Lorsque le serveur ajoute un Bot, ajoutez l'enregistrement (ID serveur, "$") à la table "guilde"
  3. Changez la table "guilde" quand une commande comme $ prefix> arrive

C'est une méthode pour changer le préfixe pour chaque serveur, mais cela consiste à changer la partie qui a été directement passée en tant que "$" dans __init__ de Bot avec command_prefix en None et ajouter séparément un collout appelé get_prefix Créez-le. discord.py vérifie ce get_prefix à chaque fois qu'un message est touché, vous pouvez donc y obtenir l'ID du serveur et obtenir les informations de la base de données.

Pour que le serveur reçoive l'événement auquel le Bot a été ajouté, le gestionnaire d'événements ʻon_guild_joinintroduit la dernière fois doit être défini. Compte tenu de ceux-ci,. / Src / app / dbot / core / bot.py` peut être modifié comme suit.

python:./src/app/dbot/core/bot.py


import discord
from discord.ext import commands
from dbot.models.guild import Guild
import traceback


class DBot(commands.Bot):
    def __init__(self, token):
        self.token = token
        super().__init__(command_prefix=None)
        self.load_cogs()

    async def get_prefix(self, message: discord.Message):
        guild = await Guild(message.guild.id).get()
        if guild:
            print("serveur:", message.guild.name)
            print("Préfixe:", guild.prefix)
            return guild.prefix
        else:
            guild = await Guild.create(message.guild.id)
            guild = await guild.get()
            print("serveur:", message.guild.name)
            print("Préfixe:", guild.prefix)
            return guild.prefix

    async def on_guild_join(self, guild: discord.Guild):
        guild = await Guild.create(guild.id)
        guild = await guild.get()
        print("serveur:", guild.name)
        print("Préfixe:", guild.prefix)
   
   #Abréviation

Ce que nous faisons est simple: nous obtenons et insérons des enregistrements basés sur l'ID du serveur.

Il ne vous reste plus qu'à implémenter la commande $ prefix. Créons un Cog appelé ʻUtilset définissons-y$ prefix`.

python:./src/app/cogs/Utils.py


import discord
from discord.ext import commands
from discord.ext.commands.errors import (
    MissingPermissions,
    MissingRequiredArgument
)
import random
from dbot.core.bot import DBot
from dbot.models.guild import Guild


class Utils(commands.Cog):
    def __init__(self, bot: DBot):
        self.bot = bot

    @commands.command(ignore_extra=False)
    @commands.has_permissions(administrator=True)
    async def prefix(self, ctx: commands.Context, *, prefix: str):
        if len(prefix) > 8:
            return await ctx.send("Le préfixe ne doit pas dépasser 8 caractères")
        guild = await Guild(ctx.guild.id).get()
        await Guild(ctx.guild.id).set(prefix=prefix)
        await ctx.send(f"Préfixe{guild.prefix}De{prefix}Changé en")

    @prefix.error
    async def on_prefix_error(self, ctx: commands.Context, error):
        if isinstance(error, MissingPermissions):
            return await ctx.send('Seul l'administrateur peut exécuter')
        if isinstance(error, MissingRequiredArgument):
            return await ctx.send('Pour l'argument, passez le nouveau préfixe dans les 8 caractères')
        raise error


def setup(bot):
    return bot.add_cog(Utils(bot))

Le préfixe peut maintenant être changé: tada:

Image from Gyazo

En se concentrant sur l'argument de la commande prefix, il y a un * à la position correspondant au troisième argument, qui est aussi [une des syntaxes de Python](https://qiita.com/LouiS0616/items/1bbe0a9bb93054f6c380#%E3%82 % A2% E3% 82% B9% E3% 82% BF% E3% 83% AA% E3% 82% B9% E3% 82% AF -% E4% BB% AE% E5% BC% 95% E6% 95 % B0% E5% 90% 8D% E3% 83% 8A% E3% 82% B7). En utilisant ce «*» dans discord.py, le comportement sera le suivant.

Image from Gyazo

Comme vous pouvez le voir, même s'il y a des espaces entre les deux, ces zones sont considérées comme un seul argument.

Référence: https://discordpy.readthedocs.io/ja/latest/ext/commands/commands.html#keyword-only-arguments

À la fin

Vous pouvez maintenant vous connecter à la base de données et créer des commandes plus complexes.

La prochaine fois, nous implémenterons la fonction ** send ** de la voix.

Recommended Posts

Discord Bot avec fonction d'enregistrement commençant par Python: (3) Coopération avec la base de données
Discord Bot avec fonction d'enregistrement commençant par Python: (1) Introduction discord.py
Discord Bot avec fonction d'enregistrement commençant par Python: (4) Lire des fichiers musicaux
Discord Bot avec fonction d'enregistrement commençant par Python: (5) Exploitez directement l'API Discord
Discord bot raspberry pi zéro avec python [Note]
Python à partir de Windows 7
GRPC commençant par Python
Comment faire fonctionner l'API Discord avec Python (enregistrement de bot)
Un débutant en Python lance Discord Bot
Apprentissage amélioré à partir de Python
Python commençant par Hello world!
Créez un bot Mastodon avec une fonction pour répondre automatiquement avec Python
Analyse de données à partir de python (visualisation de données 1)
Analyse de données à partir de python (visualisation de données 2)
Commerce système à partir de Python3: investissement à long terme
Créer un décorateur de fonction Python avec Class
"Commerce du système à partir de Python3" lecture du mémo
Efficacité commerciale à partir de zéro avec Python
Lancez le bot Discord Python pendant 24 heures.
Apprendre Python! Comparaison avec Java (fonction de base)
Analyse de données à partir de python (pré-traitement des données-apprentissage automatique)
"Première recherche élastique" commençant par un client python
Faisons un bot Twitter avec Python!
fonction python ①
[Python] fonction
Si vous voulez créer un bot discord avec python, utilisons un framework
fonction python ②
Apprentissage automatique à partir de Python Personal Memorandum Part2
Apprentissage automatique à partir de Python Personal Memorandum Part1
Créer un LINE BOT avec Minette pour Python
LINE BOT avec Python + AWS Lambda + API Gateway
J'ai essayé la synthèse de fonctions et le curry avec python
Remarque pour le formatage des nombres avec la fonction de format python
Étapes pour créer un bot Twitter avec Python
J'ai essayé d'utiliser Linux avec Discord Bot
ARC037 Baum teste poliment avec la fonction récursive Python
Made Mattermost Bot avec Python (+ Flask)
Trading système commençant par Python 3: investissement et risque
Créez un bot discord qui notifie unilatéralement avec python (seuls les requêtes et json sont utilisés)