diff --git a/.forgejo/publish.yml b/.forgejo/publish.yml new file mode 100644 index 0000000..3e18079 --- /dev/null +++ b/.forgejo/publish.yml @@ -0,0 +1,15 @@ +steps: + publish: + image: woodpeckerci/plugin-docker-buildx:2 + settings: + labels: + platform: linux/amd64 + repo: git.mylloon.fr/${CI_REPO,,} + auto_tag: true + registry: git.mylloon.fr + username: ${CI_REPO_OWNER} + password: + from_secret: cb_token + when: + event: push + branch: main diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 5553c59..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,65 +0,0 @@ -############################################################### -# Setting I use for cleaning up image tags # -# - Running cleanup every week # -# - Keeping 1 tag per image name matching : (?:v.\d+|dev) # -# - Removing tags older than 7 days matching the default : .* # -############################################################### - -image: docker:stable - -stages: - - build - - push - -services: - - docker:dind - -before_script: - - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY - -Build: - stage: build - script: - - docker pull $CI_REGISTRY_IMAGE:latest || true - - > - docker build - --pull - --build-arg VCS_REF=$CI_COMMIT_SHA - --build-arg VCS_URL=$CI_PROJECT_URL - --cache-from $CI_REGISTRY_IMAGE:latest - --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - . - - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - -Push latest: - variables: - GIT_STRATEGY: none - stage: push - only: - - main - script: - - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest - - docker push $CI_REGISTRY_IMAGE:latest - -# Push dev: -# variables: -# GIT_STRATEGY: none -# stage: push -# only: -# - dev -# script: -# - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -# - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:dev -# - docker push $CI_REGISTRY_IMAGE:dev - -Push tag: - variables: - GIT_STRATEGY: none - stage: push - only: - - tags - script: - - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME - - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME diff --git a/Dockerfile b/Dockerfile index afc274a..de3bb43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,12 @@ -FROM python:3.9.5-slim +FROM python:3.11.6-alpine3.18 + +RUN apk add dumb-init COPY requirements.txt . RUN pip install -r requirements.txt -WORKDIR /opt +WORKDIR /app COPY src . +COPY LICENSE . -CMD [ "python", "-u", "./main.py" ] +CMD [ "dumb-init", "python", "-u", "./main.py" ] diff --git a/README.md b/README.md index 593c31f..57fbe5e 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,47 @@ -# Simple Bot Twitch en Python utilisant la librairie [TwitchIO](https://github.com/TwitchIO/TwitchIO). +# Tom, a twitch bot -## Setup and Run - -### Locally - -Requires Python `3.9.5`. - -Install necessary packages: - -```batch -python3 -m pip install -r requirements.txt -``` - -Rename `.envexample` file in `.env` in root folder, then open the file and complete the fields, there is an explanation below: - -| Field | Explanation | -|----------------|-------------| -| `ACCESS_TOKEN` | Access token of the account you created for your bot that you can take [here](https://twitchtokengenerator.com/) (be sure you selected Bot Chat Token) | -| `PREFIX` | Prefix you want to use for your bot | -| `CHANNEL` | The name of the your Twitch channel you want the bot to run at (separate by comma) | - -Start Bot: - -```batch -cd src -python3 main.py -``` - -### With Docker - -With a [docker-compose](https://gitlab.com/ConfrerieDuKassoulait/Bot-Tom/-/blob/main/docker-compose.yml) or in command line: - -```batch -docker run -d \ - --name="TwitchBot" \ - registry.gitlab.com/confreriedukassoulait/bot-tom:latest \ - --ACCESS_TOKEN="yourAccessToken" \ - --PREFIX="yourPrefix" \ - --CHANNEL="yourChannel(s)" \ - -v /here/your/path/:/opt/db - -``` +[![status-badge](https://ci.mylloon.fr/api/badges/72/status.svg)](https://ci.mylloon.fr/repos/72) ## Features -Here the list of available commands by default: +Here is the list of available commands by default: -| Command | Alias | Explanation | -|-----------|-----------------------|-------------| -| `add` | | Ajoute une commande de la base de donnée du bot : `add` `omDeLaCommande` `messageDeLaCommande` | -| `remove` | `delete` | Supprime une commande de la base de donnée du bot : `remove` `nomDeLaCommande` | -| `list` | `commande.s` / `help` | Affiche la liste des commandes (base de donnée + intégré au bot) | -| `edit` | | Modifie une commande de la base de donnée du bot : `add` `nomDeLaCommande` `nouveauMessageDeLaCommande` | +| Command | Alias | Explanation | +| -------- | ------------------------------- | ------------------------------------------------------------------- | +| `add` | | Add command to the database (`add command_name command_message`) | +| `remove` | `delete` | Remove command from database (`remove command_name`) | +| `list` | `commande`, `commandes`, `help` | Print all available commands (internal and user-defined) | +| `edit` | | Modify a database command (`edit command_name new_command_message`) | + +## Setup with Docker + +- Via [docker-compose](./docker-compose.yml) +- Via command line + ```bash + docker run -d \ + --name="Bot-Tom" \ + git.mylloon.fr/confreriedukassoulait/tom:latest \ + --ACCESS_TOKEN="yourAccessToken" \ + --PREFIX="yourPrefix" \ + --CHANNEL="yourChannel(s)" \ + -v /here/your/path/:/opt/db + ``` + +## Setup Locally + +- Requires Python `3.11.6` +- Install dependencies + ```bash + python3 -m pip install -r requirements.txt + ``` +- Rename `.envexample` file in `.env`, then open the file and complete the fields + | Field | Explanation | + | -------------- | ------------------------------------------------------------------------------------------------------------ | + | `ACCESS_TOKEN` | [Access token of the bot account](https://twitchtokengenerator.com/) (be sure you selected `Bot Chat` Token) | + | `PREFIX` | Prefix used for your bot | + | `CHANNEL` | Twitch channel(s) where the bot will run (separate by comma) | + +- Start bot + ```bash + cd src; python3 main.py + ``` diff --git a/docker-compose.yml b/docker-compose.yml index 97479d2..52a5f68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "2.1" services: - bot-tom: - image: registry.gitlab.com/confreriedukassoulait/bot-tom:latest + tom: + image: git.mylloon.fr/confreriedukassoulait/tom:latest container_name: Bot-Tom environment: - ACCESS_TOKEN=yourAccessToken diff --git a/requirements.txt b/requirements.txt index 7840f5d..6ade626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -twitchio==2.0.2 -python_dotenv==0.19.0 +twitchio==2.8.2 +python_dotenv==1.0.1 diff --git a/src/main.py b/src/main.py index 34f28d7..bcdead2 100644 --- a/src/main.py +++ b/src/main.py @@ -3,30 +3,40 @@ from os import listdir from utils.core import load from utils.commands import CommandesDB + class Client(commands.Bot): def __init__(self): self.keys = load(["ACCESS_TOKEN", "PREFIX", "CHANNEL"]) - super().__init__(token = self.keys["ACCESS_TOKEN"], prefix = self.keys["PREFIX"], initial_channels = self.keys["CHANNEL"]) + super().__init__( + token=self.keys["ACCESS_TOKEN"], + prefix=self.keys["PREFIX"], + initial_channels=self.keys["CHANNEL"], + ) async def event_ready(self): CommandesDB().creationTable() print(f"Logged in as {self.nick}") async def event_message(self, message): - if message.echo: # Messages with echo set to True are messages sent by the bot + if message.echo: # Messages with echo set to True are messages sent by the bot return - await self.handle_commands(message) # Let the bot know we want to handle and invoke our commands - + await self.handle_commands( + message + ) # Let the bot know we want to handle and invoke our commands + async def event_command_error(self, _, error): - if isinstance(error, commands.errors.CommandNotFound): # Ignore unknown commands (useful because custom commands aren’t known) + if isinstance( + error, commands.errors.CommandNotFound + ): # Ignore unknown commands (useful because custom commands aren’t known) return raise error + client = Client() for file in listdir("modules"): - if file.endswith(".py") and file.startswith("-") == False: + if file.endswith(".py") and file.startswith("-") is False: client.load_module(f"modules.{file[:-3]}") client.run() diff --git a/src/modules/commandes.py b/src/modules/commandes.py index 81ee51a..7abb434 100644 --- a/src/modules/commandes.py +++ b/src/modules/commandes.py @@ -2,10 +2,13 @@ from twitchio.ext import commands from utils.core import load, listCommands from utils.commands import CommandesDB, existeCommande, existeTouteCommande + def prepare(client: commands.Bot): client.add_cog(Commandes(client)) -class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai ne sont autorisés qu'aux modérateurs + +class Commandes(commands.Cog): + # Les méthodes qui ont no_global_checks = Vrai ne sont autorisés qu'aux modérateurs""" def __init__(self, client: commands.Bot): self.client = client self.keys = load(["PREFIX"]) @@ -14,12 +17,12 @@ class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai self.alreadyExistingCommand = "cette commande existe déjà" @commands.command(name="add", no_global_checks=True) - async def _add(self, ctx: commands.Context, commandName = None, commandMessage = None): + async def _add(self, ctx: commands.Context, commandName=None, commandMessage=None): """Ajoute une commande de la base de donnée du bot : add nomDeLaCommande messageDeLaCommande""" - if commandName == None or commandMessage == None: + if commandName is None or commandMessage is None: return - if ctx.author.is_mod: - if existeTouteCommande(self.client, commandName)[0] == False: + if ctx.author.is_mod: # type: ignore + if existeTouteCommande(self.client, commandName)[0] is False: CommandesDB().ajoutCommande(commandName, commandMessage) await ctx.send(f"@{ctx.author.name}, commande {commandName} ajoutée !") else: @@ -28,14 +31,16 @@ class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai await ctx.send(f"@{ctx.author.name}, {self.notModo}.") @commands.command(name="remove", aliases=["delete"], no_global_checks=True) - async def _remove(self, ctx: commands.Context, commandName = None): + async def _remove(self, ctx: commands.Context, commandName=None): """Supprime une commande de la base de donnée du bot : remove nomDeLaCommande""" - if commandName == None: + if commandName is None: return - if ctx.author.is_mod: + if ctx.author.is_mod: # type: ignore if existeCommande(commandName)[0]: CommandesDB().suppressionCommande(commandName) - await ctx.send(f"@{ctx.author.name}, commande {commandName} supprimée !") + await ctx.send( + f"@{ctx.author.name}, commande {commandName} supprimée !" + ) else: await ctx.send(f"@{ctx.author.name}, {self.notExistingCommand}.") else: @@ -49,7 +54,7 @@ class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai for command in listCommands(self.client): name = command.name if command.no_global_checks: - if not ctx.author.is_mod: + if not ctx.author.is_mod: # type: ignore continue if command.aliases: name += " (alias: " @@ -64,14 +69,16 @@ class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai message += f"{self.keys['PREFIX']}{commande[0]}, " await ctx.send(message[:-2]) else: - await ctx.send(f"@{ctx.author.name}, aucune commande enrengistrée dans la base de donnée.") + await ctx.send( + f"@{ctx.author.name}, aucune commande enrengistrée dans la base de donnée." + ) @commands.command(name="edit", no_global_checks=True) - async def _edit(self, ctx: commands.Context, commandName = None, commandMessage = None): + async def _edit(self, ctx: commands.Context, commandName=None, commandMessage=None): """Modifie une commande de la base de donnée du bot : add nomDeLaCommande nouveauMessageDeLaCommande""" - if commandName == None or commandMessage == None: + if commandName is None or commandMessage is None: return - if ctx.author.is_mod: + if ctx.author.is_mod: # type: ignore if existeCommande(commandName)[0]: CommandesDB().suppressionCommande(commandName) CommandesDB().ajoutCommande(commandName, commandMessage) @@ -81,9 +88,12 @@ class Commandes(commands.Cog): # Les méthodes qui ont no_global_checks de Vrai else: await ctx.send(f"@{ctx.author.name}, {self.notModo}.") - @commands.Cog.event() + @commands.Cog.event() # type: ignore async def event_message(self, message): if message.content.startswith(self.keys["PREFIX"]): - command = existeCommande(message.content[1:].split(" ")[0]) # récupère le nom de la commande - if command[0]: # vérification si existe - await message.channel.send(f"@{message.author.name}, {command[1]}") # envois le contenu de la commande + command = existeCommande( + message.content[1:].split(" ")[0] + ) # récupère le nom de la commande + if command[0]: # vérification si existe + # envois le contenu de la commande + await message.channel.send(f"@{message.author.name}, {command[1]}") # type: ignore diff --git a/src/utils/commands.py b/src/utils/commands.py index 9f31741..e402b08 100644 --- a/src/utils/commands.py +++ b/src/utils/commands.py @@ -1,6 +1,7 @@ from utils.db import Database from utils.core import listCommands + class CommandesDB(Database): def __init__(self): super().__init__(r"db/bot.sqlite3") @@ -14,7 +15,7 @@ class CommandesDB(Database): ); """ self.requete(requete) - + def ajoutCommande(self, name: str, message: str): """Ajoute une commande.""" requete = """ @@ -26,7 +27,7 @@ class CommandesDB(Database): ); """ self.requete(requete, [name, message]) - + def suppressionCommande(self, name: str): """Supprime une commande.""" requete = """ @@ -39,6 +40,7 @@ class CommandesDB(Database): """Retourne la liste des commandes.""" return self.affichageResultat(self.requete("SELECT * FROM commandes;")) + def existeCommande(command: str): """Vérifie qu'une commande existe dans la base de donnée.""" commandes = CommandesDB().listeCommande() @@ -47,6 +49,7 @@ def existeCommande(command: str): return (True, commande[1]) return (False,) + def existeTouteCommande(client, commande: str): """Vérifie qu'une commande existe dans la base de donnée et dans le bot en lui-même.""" commandes = [] diff --git a/src/utils/core.py b/src/utils/core.py index 4c81a7b..38d500e 100644 --- a/src/utils/core.py +++ b/src/utils/core.py @@ -2,6 +2,7 @@ from os import environ from dotenv import load_dotenv from sys import exit + def load(variables): """Load env variables""" keys = {} @@ -10,7 +11,8 @@ def load(variables): try: res = environ[var] if var == "CHANNEL": - res = list(set(res.split(',')) - {""}) # create a list for the channels and remove blank channels and doubles + # create a list for the channels and remove blank channels and doubles + res = list(set(res.split(",")) - {""}) keys[var] = res except KeyError: print(f"Please set the environment variable {var} (.env file supported)") @@ -18,6 +20,7 @@ def load(variables): return keys + def listCommands(client): cogs = client.cogs.values() commands = [] diff --git a/src/utils/db.py b/src/utils/db.py index 275171b..c2ff2aa 100644 --- a/src/utils/db.py +++ b/src/utils/db.py @@ -1,8 +1,12 @@ import sqlite3 + class Database: def __init__(self, urlDatabase: str): - self.connexion = self.createConnection(urlDatabase) + connexion = self.createConnection(urlDatabase) + if connexion is None: + raise Exception("Can't connect to database") + self.connexion = connexion def createConnection(self, path): """Connexion à une base de donnée SQLite""" @@ -24,14 +28,14 @@ class Database: else: return True - def requete(self, requete, valeurs = None): + def requete(self, requete, valeurs=None): """Envois une requête vers la base de données""" try: curseur = self.connexion.cursor() if valeurs: if type(valeurs) not in [list, tuple]: valeurs = [valeurs] - curseur.execute(requete, valeurs) + curseur.execute(requete, valeurs) else: curseur.execute(requete) self.connexion.commit() @@ -42,7 +46,7 @@ class Database: def affichageResultat(self, curseur): """Affiche le résultat d'une requête""" tableau = [] - if curseur == None: + if curseur is None: return tableau lignes = curseur[0].fetchall() for ligne in lignes: