diff --git a/.github/workflows/define-build-image.yml b/.github/workflows/define-build-image.yml new file mode 100644 index 0000000..558cc55 --- /dev/null +++ b/.github/workflows/define-build-image.yml @@ -0,0 +1,31 @@ +name: Build image + +on: + workflow_call: + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: [self-hosted, linux] + steps: + - uses: actions/checkout@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: lowercase github.repository + run: | + echo "IMAGE_NAME=`echo ${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV} + - name: Build + uses: docker/build-push-action@v6 + with: + context: . + push: false + tags: ${{ env.IMAGE_NAME }}:latest + outputs: type=docker,dest=/tmp/image.tar + - name: Upload image artifact + uses: actions/upload-artifact@v4 + with: + name: image + path: /tmp/image.tar diff --git a/.github/workflows/define-pylint.yml b/.github/workflows/define-pylint.yml new file mode 100644 index 0000000..e7a8de4 --- /dev/null +++ b/.github/workflows/define-pylint.yml @@ -0,0 +1,35 @@ +name: Define Pylint + +on: + workflow_call: + inputs: + python-version: + required: true + type: string + +defaults: + run: + shell: bash + +jobs: + pylint: + runs-on: + - ${{ matrix.os }} + - self-hosted + strategy: + matrix: + os: [Linux] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ inputs.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.github/workflows/release-tagged-image.yml b/.github/workflows/release-tagged-image.yml new file mode 100644 index 0000000..bb03301 --- /dev/null +++ b/.github/workflows/release-tagged-image.yml @@ -0,0 +1,90 @@ +name: Release tagged image + +on: [workflow_dispatch] + +env: + CURRENT_VERSION: 0.0.5 + +defaults: + run: + shell: bash + +jobs: + pylint: + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + uses: ./.github/workflows/define-pylint.yml + with: + python-version: ${{ matrix.python-version }} + secrets: inherit + + build: + needs: [pylint] + uses: ./.github/workflows/define-build-image.yml + secrets: inherit + + tag: + runs-on: self-hosted + steps: + - uses: mukunku/tag-exists-action@v1.6.0 + id: check-tag + with: + tag: ${{ env.CURRENT_VERSION }} + - name: Fail if tag exists + if: steps.check-tag.outputs.exists == 'true' + run: | + echo "Tag ${{ env.CURRENT_VERSION }} exists!" + exit 1 + - name: Print tag if it doesn't exist + if: steps.check-tag.outputs.exists == 'false' + run: | + echo "Tag ${{ env.CURRENT_VERSION }} doesn't yet exist and can be created" + + push: + needs: [tag, build] + runs-on: [self-hosted, linux] + permissions: + contents: read + packages: write + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: lowercase github.repository + run: | + echo "IMAGE_NAME=`echo ${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV} + - name: Download image artifact + uses: actions/download-artifact@v4 + with: + name: image + path: /tmp + - name: Load image + run: | + docker load --input /tmp/image.tar + - name: Push + run: | + docker tag ${{ env.IMAGE_NAME }}:latest ghcr.io/${{ env.IMAGE_NAME }}:${{ env.CURRENT_VERSION }} + docker push ghcr.io/${{ env.IMAGE_NAME }}:${{ env.CURRENT_VERSION }} + docker tag ghcr.io/${{ env.IMAGE_NAME }}:${{ env.CURRENT_VERSION }} ghcr.io/${{ env.IMAGE_NAME }}:latest + docker push ghcr.io/${{ env.IMAGE_NAME }}:latest + + release: + needs: [push] + runs-on: [self-hosted, linux] + permissions: + contents: write + steps: + - name: Add body.md + run: | + touch body.md + - name: Create new release + uses: ncipollo/release-action@v1 + with: + bodyFile: "body.md" + tag: "${{ env.CURRENT_VERSION }}" + commit: "main" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..a04d252 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,21 @@ +name: Run Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + pylint: + strategy: + matrix: + python-version: ["3.10", "3.12"] + uses: ./.github/workflows/define-pylint.yml + with: + python-version: ${{ matrix.python-version }} + secrets: inherit + build: + needs: [pylint] + uses: ./.github/workflows/define-build-image.yml + secrets: inherit diff --git a/.pylintrc b/.pylintrc index 2d586cb..44c492e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,7 @@ +[MASTER] + +ignore=tests + [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show @@ -22,7 +26,9 @@ disable=too-many-arguments, too-many-instance-attributes, no-name-in-module, isinstance-second-argument-not-valid-type, - multiple-statements + multiple-statements, + relative-beyond-top-level, + too-many-positional-arguments [FORMAT] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b518ec7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-bookworm + +RUN apt-get update \ + && useradd -m nikobot \ + && mkdir /home/nikobot/src \ + && chown nikobot /home/nikobot/src + +COPY --chown=nikobot --chmod=755 src/ /home/nikobot/src/ +COPY --chown=nikobot --chmod=755 requirements.txt /home/nikobot/requirements.txt + +# install pip packages +RUN pip install -r /home/nikobot/requirements.txt + +USER nikobot + +ENTRYPOINT [ "python3", "/home/nikobot/src/main.py" ] diff --git a/src/main.py b/src/main.py index 9f7def2..5a33342 100644 --- a/src/main.py +++ b/src/main.py @@ -25,9 +25,7 @@ def __init__(self) -> None: def start_bot(self): """Start the discord bot""" - with open("dc_token.txt", "r", encoding="utf8") as file: - token = file.readlines()[0] - self.run(token) + self.run(os.environ["DISCORD_TOKEN"]) async def setup_hook(self) -> None: util.VolatileStorage["modules"] = [] diff --git a/src/nikobot/modules/avatar.py b/src/nikobot/modules/avatar.py index 6f5abef..9616f94 100644 --- a/src/nikobot/modules/avatar.py +++ b/src/nikobot/modules/avatar.py @@ -31,18 +31,18 @@ async def avatar(self, ctx: commands.context.Context | discord.interactions.Inte await util.discord.reply(ctx, "User has a default avatar, which can't be downloadad.") return - os.makedirs(f"{util.VolatileStorage["cache_dir"]}/avatars", exist_ok=True) + os.makedirs(f"{util.VolatileStorage['cache_dir']}/avatars", exist_ok=True) # Download the user's avatar response = requests.get(user_obj.avatar.url, timeout=30) if response.status_code == 200: - with open(f"{util.VolatileStorage["cache_dir"]}/avatars/{user_obj}.png", "wb") as f: + with open(f"{util.VolatileStorage['cache_dir']}/avatars/{user_obj}.png", "wb") as f: f.write(response.content) else: await util.discord.reply(ctx, "Failed to download avatar.") # send the avatar - avatar_file = discord.File(f"{util.VolatileStorage["cache_dir"]}/avatars/{user_obj}.png", + avatar_file = discord.File(f"{util.VolatileStorage['cache_dir']}/avatars/{user_obj}.png", filename=f"{user_obj}.png") await util.discord.reply(ctx, f"Profile picture of {user_obj.nick or user_obj.display_name or user_obj.name}:", diff --git a/src/nikobot/modules/help.py b/src/nikobot/modules/help.py index c9a40ea..f025bd9 100644 --- a/src/nikobot/modules/help.py +++ b/src/nikobot/modules/help.py @@ -125,7 +125,7 @@ def _generate_help_specific_normal(self, command_name: str) -> discord.Embed: desc = cmd.description.strip("__hidden__") if "." in cmd.name: - desc += f"\nCommand is a part of the '{cmd.name.split(".", maxsplit=1)[0]}' module" + desc += f"\nCommand is a part of the '{cmd.name.split('.', maxsplit=1)[0]}' module" answer.add_field(name="Description", value=desc) return answer diff --git a/src/nikobot/modules/mal/malnotifier.py b/src/nikobot/modules/mal/malnotifier.py index cd61b86..9c7aaf6 100644 --- a/src/nikobot/modules/mal/malnotifier.py +++ b/src/nikobot/modules/mal/malnotifier.py @@ -1,5 +1,6 @@ """A module containing MyAnimeList-related commands""" +import os from datetime import datetime, timedelta from threading import Thread @@ -14,9 +15,6 @@ # pylint: disable=protected-access -with open("client_id.txt", "r", encoding="utf8") as file: - CLIENT_ID = file.readlines()[0] - command_group = app_commands.Group( name="mal", description="The module for MyAnimeList-related commands" @@ -204,7 +202,7 @@ def import_users(self): async def setup(bot: commands.Bot): """Setup the bot_commands cog""" - util.VolatileStorage["mal.CLIENT-ID"] = CLIENT_ID + util.VolatileStorage["mal.CLIENT-ID"] = os.environ["MAL_CLIENT_ID"] mal_helper._setup() manganato_helper._setup() diff --git a/src/nikobot/modules/mal/manga.py b/src/nikobot/modules/mal/manga.py index e1386bc..d9aa3e1 100644 --- a/src/nikobot/modules/mal/manga.py +++ b/src/nikobot/modules/mal/manga.py @@ -8,7 +8,7 @@ import requests from PIL import Image -from discord import Embed, File, Color +from discord import Embed, File from . import error, mal_helper, manganato_helper from .chapter import Chapter @@ -165,7 +165,7 @@ def picture_file(self) -> str: path = os.path.join(util.VolatileStorage["cache_dir"], "mal") os.makedirs(path, exist_ok=True) - path = os.path.join(path, f"preview_{self.mal_id}.{self.picture_url.rsplit(".", maxsplit=1)[1]}") + path = os.path.join(path, f"preview_{self.mal_id}.{self.picture_url.rsplit('.', maxsplit=1)[1]}") if os.path.isfile(path): return path diff --git a/src/nikobot/modules/music.py b/src/nikobot/modules/music.py index 1568309..16791b6 100644 --- a/src/nikobot/modules/music.py +++ b/src/nikobot/modules/music.py @@ -1,3 +1,5 @@ +# pylint: skip-file + import discord import youtube_dl from discord.ext import commands, tasks @@ -100,8 +102,9 @@ async def stop (self, ctx: commands.context.Context): @tasks.loop(seconds=5) async def song_scheduler(self): """manages the queue""" - - for guild_id in list(self.active_plays.keys()): # list() to create a new object which can't change during the loop execution + + # list() to create a new object which can't change during the loop execution + for guild_id in list(self.active_plays.keys()): data = self.active_plays[guild_id] if not data["playing"] and len(data["urls"]) > 0: @@ -113,8 +116,13 @@ async def song_scheduler(self): async def play_song(self, url: str, guild_id: int): """ plays back the given song """ - FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'} - YDL_OPTIONS = {'format' : "bestaudio"} + FFMPEG_OPTIONS = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn' + } + YDL_OPTIONS = { + 'format' : "bestaudio" + } voice_clients: list[discord.voice_client.VoiceClient] = self.bot.voice_clients vc: discord.voice_client.VoiceClient = [item for item in voice_clients if item.guild.id==guild_id][0] with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..388083e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# pylint: skip-file diff --git a/tests/test_aspect_parser.py b/tests/test_aspect_parser.py index 2a5c76d..eb3b385 100644 --- a/tests/test_aspect_parser.py +++ b/tests/test_aspect_parser.py @@ -1,3 +1,5 @@ +# pylint: skip-file + from src.modules.tc4 import AspectParser def test_create(): diff --git a/tests/test_shortest_path2.py b/tests/test_shortest_path2.py index caf24ad..0a182b4 100644 --- a/tests/test_shortest_path2.py +++ b/tests/test_shortest_path2.py @@ -1,3 +1,5 @@ +# pylint: skip-file + from src.modules.tc4 import AspectParser from src.modules.tc4.shortest_path import ShortestPath from src.modules.tc4.shortest_path2 import ShortestPath2