Merge branch '18-log-into-discord-channel' into 'develop'
Resolve "Log into discord channel" Closes #18 See merge request ekzyis/musicube!20
This commit is contained in:
commit
6d805e11be
|
@ -1,3 +1,5 @@
|
||||||
DISCORD_BOT_TOKEN=
|
DISCORD_BOT_TOKEN=
|
||||||
DISCORD_CMD_PREFIX=
|
DISCORD_CMD_PREFIX=
|
||||||
|
DISCORD_LOG_CHANNEL_ID=
|
||||||
|
DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP=False
|
||||||
YOUTUBE_COOKIES=youtube.com_cookies.txt
|
YOUTUBE_COOKIES=youtube.com_cookies.txt
|
|
@ -12,7 +12,8 @@ disable=
|
||||||
wrong-import-position,
|
wrong-import-position,
|
||||||
redefined-outer-name,
|
redefined-outer-name,
|
||||||
invalid-name,
|
invalid-name,
|
||||||
no-self-use
|
no-self-use,
|
||||||
|
too-many-instance-attributes
|
||||||
|
|
||||||
[FORMAT]
|
[FORMAT]
|
||||||
indent-string=' '
|
indent-string=' '
|
||||||
|
|
|
@ -12,6 +12,9 @@ RUN pip install -r requirements.txt
|
||||||
ARG GIT_COMMIT=unset
|
ARG GIT_COMMIT=unset
|
||||||
ARG DISCORD_BOT_TOKEN=unset
|
ARG DISCORD_BOT_TOKEN=unset
|
||||||
ARG YOUTUBE_COOKIES=unset
|
ARG YOUTUBE_COOKIES=unset
|
||||||
|
ARG DISCORD_LOG_CHANNEL_ID=unset
|
||||||
|
ARG DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP=unset
|
||||||
ENV DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} YOUTUBE_COOKIES=${YOUTUBE_COOKIES} GIT_COMMIT=${GIT_COMMIT}
|
ENV DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} YOUTUBE_COOKIES=${YOUTUBE_COOKIES} GIT_COMMIT=${GIT_COMMIT}
|
||||||
|
ENV DISCORD_LOG_CHANNEL_ID=${DISCORD_LOG_CHANNEL_ID} DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP=${DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP}
|
||||||
|
|
||||||
CMD ["python", "src/bot.py"]
|
CMD ["python", "src/bot.py"]
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -10,5 +10,7 @@ build:
|
||||||
--build-arg DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} \
|
--build-arg DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} \
|
||||||
--build-arg YOUTUBE_COOKIES=${YOUTUBE_COOKIES} \
|
--build-arg YOUTUBE_COOKIES=${YOUTUBE_COOKIES} \
|
||||||
--build-arg GIT_COMMIT=${GIT_COMMIT} \
|
--build-arg GIT_COMMIT=${GIT_COMMIT} \
|
||||||
|
--build-arg DISCORD_LOG_CHANNEL_ID=${DISCORD_LOG_CHANNEL_ID} \
|
||||||
|
--build-arg DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP=${DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP} \
|
||||||
-t musicube:${GIT_COMMIT} -t musicube:latest \
|
-t musicube:${GIT_COMMIT} -t musicube:latest \
|
||||||
.
|
.
|
||||||
|
|
29
src/bot.py
29
src/bot.py
|
@ -2,6 +2,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from distutils.util import strtobool
|
||||||
|
import logging
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
|
@ -10,6 +12,7 @@ import youtube_dl
|
||||||
|
|
||||||
from error import ErrorHandler
|
from error import ErrorHandler
|
||||||
from message import NowPlayingMessage, QueuedMessage, ErrorMessage
|
from message import NowPlayingMessage, QueuedMessage, ErrorMessage
|
||||||
|
from log import create_logger, DiscordLogger
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ class Song:
|
||||||
|
|
||||||
|
|
||||||
class Music(commands.Cog):
|
class Music(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot: commands.Bot, logger: logging.Logger = None):
|
||||||
self._bot = bot
|
self._bot = bot
|
||||||
self._queue = asyncio.Queue()
|
self._queue = asyncio.Queue()
|
||||||
self._queue_lock = asyncio.Lock()
|
self._queue_lock = asyncio.Lock()
|
||||||
|
@ -52,6 +55,15 @@ class Music(commands.Cog):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
self._handle_playback.start()
|
self._handle_playback.start()
|
||||||
|
|
||||||
|
self.logger = logger if logger else create_logger("bot")
|
||||||
|
if log_channel_id := os.environ.get("DISCORD_LOG_CHANNEL_ID", None):
|
||||||
|
clear_on_startup = os.environ.get("DISCORD_LOG_CHANNEL_CLEAR_ON_STARTUP", False)
|
||||||
|
if clear_on_startup == "":
|
||||||
|
clear_on_startup = False
|
||||||
|
if isinstance(clear_on_startup, str):
|
||||||
|
clear_on_startup = bool(strtobool(clear_on_startup))
|
||||||
|
self.logger.addHandler(DiscordLogger(self._bot, channel_id=log_channel_id, clear_on_startup=clear_on_startup))
|
||||||
|
|
||||||
def cog_unload(self):
|
def cog_unload(self):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
self._handle_playback.cancel()
|
self._handle_playback.cancel()
|
||||||
|
@ -79,7 +91,7 @@ class Music(commands.Cog):
|
||||||
|
|
||||||
def after(err):
|
def after(err):
|
||||||
if err:
|
if err:
|
||||||
print(f"Player error: {err}")
|
self.logger.error("Player error: %s", err)
|
||||||
self._next()
|
self._next()
|
||||||
ctx.voice_client.play(audio, after=after)
|
ctx.voice_client.play(audio, after=after)
|
||||||
embed = NowPlayingMessage(title=song.title, url=song.webpage_url)
|
embed = NowPlayingMessage(title=song.title, url=song.webpage_url)
|
||||||
|
@ -87,7 +99,7 @@ class Music(commands.Cog):
|
||||||
await self._add_skip_button(msg)
|
await self._add_skip_button(msg)
|
||||||
# pylint: disable=broad-except
|
# pylint: disable=broad-except
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(f"Error during playback: {err}")
|
self.logger.error("Error during playback: %s", err)
|
||||||
if ctx:
|
if ctx:
|
||||||
embed = ErrorMessage(str(err))
|
embed = ErrorMessage(str(err))
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
@ -154,20 +166,21 @@ class Music(commands.Cog):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
prefix = os.environ.get("DISCORD_CMD_PREFIX", "!")
|
prefix = os.environ.get("DISCORD_CMD_PREFIX", "!")
|
||||||
|
logger = create_logger("bot")
|
||||||
bot = commands.Bot(command_prefix=commands.when_mentioned_or(prefix),
|
bot = commands.Bot(command_prefix=commands.when_mentioned_or(prefix),
|
||||||
description='Relatively simple music bot example')
|
description='Relatively simple music bot example')
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f"Logged in as {bot.user} ({bot.user.id})")
|
logger.info("Logged in as %s (%s)", bot.user, bot.user.id)
|
||||||
print('------')
|
logger.info('------')
|
||||||
|
|
||||||
bot.add_cog(Music(bot))
|
bot.add_cog(Music(bot, logger=logger))
|
||||||
bot.add_cog(ErrorHandler(bot))
|
bot.add_cog(ErrorHandler(bot, logger=logger))
|
||||||
|
|
||||||
token = os.environ.get("DISCORD_BOT_TOKEN", None)
|
token = os.environ.get("DISCORD_BOT_TOKEN", None)
|
||||||
if not token:
|
if not token:
|
||||||
print("Discord bot token not found")
|
logger.warning("Discord bot token not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
bot.run(token)
|
bot.run(token)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from message import ErrorMessage
|
from message import ErrorMessage
|
||||||
|
@ -6,8 +8,9 @@ from message import ErrorMessage
|
||||||
class ErrorHandler(commands.Cog):
|
class ErrorHandler(commands.Cog):
|
||||||
"""A cog for global error handling."""
|
"""A cog for global error handling."""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot, logger: logging.Logger):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_command_error(self, ctx: commands.Context, error: commands.CommandError):
|
async def on_command_error(self, ctx: commands.Context, error: commands.CommandError):
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from logging import LogRecord
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from discord import TextChannel
|
||||||
|
from discord.ext.commands import Bot as DiscordBot
|
||||||
|
|
||||||
|
|
||||||
|
__LOG_FORMAT__ = '%(asctime)s %(name)-5s %(levelname)-8s %(message)s'
|
||||||
|
__DATE_FORMAT__ = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
|
||||||
|
def create_logger(name: str) -> logging.Logger:
|
||||||
|
"""Create logger with some sane defaults."""
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
stream_handler = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
|
||||||
|
COMMIT = os.getenv('GIT_COMMIT') or 'no-commit'
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
log_path = f"{COMMIT}_{timestamp}.log"
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(log_path)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt='%(asctime)s %(name)-5s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||||
|
stream_handler.setFormatter(formatter)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(stream_handler)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordLogger(logging.Handler):
|
||||||
|
"""Logging handler which sends logs to the specified discord channel."""
|
||||||
|
|
||||||
|
def __init__(self, bot: DiscordBot, channel_id=None, clear_on_startup=False):
|
||||||
|
super().__init__()
|
||||||
|
self._bot = bot
|
||||||
|
self._channel_id = channel_id
|
||||||
|
self._channel: TextChannel = None
|
||||||
|
self._channel_cleared = False
|
||||||
|
self._clear_on_startup = clear_on_startup
|
||||||
|
self._formatter = logging.Formatter(fmt='%(asctime)s %(name)-5s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
def emit(self, record: LogRecord) -> None:
|
||||||
|
if self._channel_id is not None:
|
||||||
|
msg = self._formatter.format(record)
|
||||||
|
|
||||||
|
async def _emit_to_channel() -> None:
|
||||||
|
if self._channel is None:
|
||||||
|
self._channel = await self._bot.fetch_channel(self._channel_id)
|
||||||
|
if self._clear_on_startup:
|
||||||
|
async for log in self._channel.history(limit=None):
|
||||||
|
await log.delete()
|
||||||
|
|
||||||
|
if self._channel is not None:
|
||||||
|
await self._channel.send(msg)
|
||||||
|
|
||||||
|
asyncio.create_task(_emit_to_channel())
|
|
@ -2,6 +2,8 @@ import asyncio
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
@ -13,6 +15,14 @@ sys.path.insert(0, SRC_PATH)
|
||||||
from bot import Music
|
from bot import Music
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def global_teardown():
|
||||||
|
yield
|
||||||
|
logs = glob.glob('*.log')
|
||||||
|
for log in logs:
|
||||||
|
os.remove(log)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def bot(mocker: MockerFixture):
|
def bot(mocker: MockerFixture):
|
||||||
bot_mock = mocker.patch('discord.ext.commands.Bot', autospec=True)
|
bot_mock = mocker.patch('discord.ext.commands.Bot', autospec=True)
|
||||||
|
|
Loading…
Reference in New Issue