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:
Ramdip Gill 2021-11-04 00:09:17 +00:00
commit 6d805e11be
8 changed files with 110 additions and 10 deletions

View File

@ -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

View File

@ -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=' '

View File

@ -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"]

View File

@ -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 \
. .

View File

@ -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)

View File

@ -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):

66
src/log.py Normal file
View File

@ -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())

View File

@ -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)