From 4267bcea77f85e15b1373dc85519efdf115795f5 Mon Sep 17 00:00:00 2001 From: Nikhil Aryal Date: Fri, 24 Mar 2023 03:35:19 +0000 Subject: [PATCH] Initial commit --- LICENSE | 19 ++++++++ README.md | 3 ++ applog/__init__.py | 0 applog/logging.yaml | 40 ++++++++++++++++ applog/utils.py | 27 +++++++++++ bot.py | 37 +++++++++++++++ cogs/system.py | 108 ++++++++++++++++++++++++++++++++++++++++++++ core/__init__.py | 0 core/common.py | 49 ++++++++++++++++++++ main.py | 37 +++++++++++++++ 10 files changed, 320 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 applog/__init__.py create mode 100644 applog/logging.yaml create mode 100644 applog/utils.py create mode 100644 bot.py create mode 100644 cogs/system.py create mode 100644 core/__init__.py create mode 100644 core/common.py create mode 100644 main.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0d23be --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Discord Bot Template + +This template is used by my Discord Bots to speed up the process of getting started. diff --git a/applog/__init__.py b/applog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/applog/logging.yaml b/applog/logging.yaml new file mode 100644 index 0000000..5128d40 --- /dev/null +++ b/applog/logging.yaml @@ -0,0 +1,40 @@ +version: 1 +disable_existing_loggers: True +formatters: + simple: + format: "%(asctime)s - [%(threadName)s|%(levelname)s] - %(message)s" +handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + info_file_handler: + class: logging.handlers.RotatingFileHandler + level: INFO + formatter: simple + filename: applog/info.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 + error_file_handler: + class: logging.handlers.RotatingFileHandler + level: ERROR + formatter: simple + filename: applog/errors.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 +loggers: + system: + level: INFO + handlers: [ console, info_file_handler ] + propogate: no + discord: + level: INFO + handlers: [ console, info_file_handler ] + propogate: no + bot: + level: INFO + handlers: [ console, info_file_handler ] + propogate: no \ No newline at end of file diff --git a/applog/utils.py b/applog/utils.py new file mode 100644 index 0000000..0347b1a --- /dev/null +++ b/applog/utils.py @@ -0,0 +1,27 @@ +"""Original Credit: https://github.com/tiangolo/fastapi/issues/290#issuecomment-500119238""" + +import logging.config +import os + +import yaml + + +def read_logging_config(default_path="logging.yaml", env_key="LOG_CFG"): + """Load the logging config into memory""" + path = default_path + value = os.getenv(env_key, None) + if value: + path = value + if os.path.exists(path): + with open(path, "rt") as f: + logging_config = yaml.safe_load(f.read()) + return logging_config + return None + + +def setup_logging(logging_config, default_level=logging.INFO): + """Configure logging""" + if logging_config: + logging.config.dictConfig(logging_config) + else: + logging.basicConfig(level=default_level) diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..1b3cab6 --- /dev/null +++ b/bot.py @@ -0,0 +1,37 @@ +"""Bot startup code""" +import logging +from pathlib import Path + +from discord.ext import commands + +from applog.utils import read_logging_config, setup_logging +import core.common as common + +logger = logging.getLogger(__name__) +discord_logger = logging.getLogger('discord') + +log_config_dict = read_logging_config("applog/logging.yaml") +setup_logging(log_config_dict) + +common.prompt_config("config.json", "Enter repository URL: ", "repo_link") +common.prompt_config("config.json", "Enter bot token: ", "token") +common.prompt_config("config.json", "Enter bot prefix: ", "prefix") + +config, _ = common.load_config("config.json") + +bot = commands.Bot(command_prefix=config['prefix']) + + +def get_extensions(): + """Gets extension list dynamically""" + extensions = [] + for file in Path("cogs").glob("**/*.py"): + extensions.append(str(file).replace("/", ".").replace(".py", "")) + return extensions + + +for ext in get_extensions(): + bot.load_extension(ext) + +logger.info("starting bot.") +bot.run(config['token']) diff --git a/cogs/system.py b/cogs/system.py new file mode 100644 index 0000000..29ed2ae --- /dev/null +++ b/cogs/system.py @@ -0,0 +1,108 @@ +"""System Module from Bot Template""" +import sys +import subprocess +import logging + +from discord.ext import commands +import discord + +import core.common as common + +logger = logging.getLogger(__name__) + + +class System(commands.Cog): + """System Cog from Bot Template""" + def __init__(self, bot): + self.bot = bot + self.commit = subprocess.run(["git", "show", "-s", "--format=%h"], + capture_output=True, + encoding="utf-8", check=True).stdout.strip() + + @commands.group() + async def bot_group(self, ctx): + """Command group for core bot commands""" + if ctx.invoked_subcommand is None: + await ctx.send('No subcommand invoked.') + + @bot_group.command() + async def ping(self, ctx): + """Ping the bot""" + await ctx.message.delete() + embed = discord.Embed(title="Pong!", + description=str(round(self.bot.latency * 1000, 1)) + "ms", + colour=common.random_rgb()) + embed.set_footer(text=f"requested by {ctx.author}", + icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) + + @bot_group.command() + @commands.is_owner() + async def stop(self, ctx): + """Stop the bot""" + await ctx.message.delete() + embed = discord.Embed(title="Stopping Bot!", + color=common.random_rgb()) + embed.set_footer(text=f"requested by {ctx.author}", + icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) + sys.exit() + + @bot_group.command() + @commands.is_owner() + async def restart(self, ctx): + """Restart the bot""" + await ctx.message.delete() + embed = discord.Embed(title="Restarting Bot!", + color=common.random_rgb()) + embed.set_footer(text=f"requested by {ctx.author}", + icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) + sys.exit(26) + + @bot_group.command() + async def version(self, ctx): + """Get bot version""" + await ctx.message.delete() + commit_date = subprocess.run(["git", "show", "-s", "--format=%ci", self.commit], + capture_output=True, check=True, + encoding="utf-8").stdout.strip() + commit_msg = subprocess.run(['git', 'show', '-s', '--format=%B', self.commit], + capture_output=True, check=True, + encoding="utf-8").stdout.strip() + embed = discord.Embed(title="Bot Version", + description="Current Bot Version", + color=common.random_rgb(self.commit)) + embed.set_footer(text=f"requested by {ctx.author}", icon_url=ctx.author.avatar_url) + embed.add_field(name="ID", value=self.commit, inline=False) + embed.add_field(name="Date", value=commit_date, inline=False) + embed.add_field(name="Changelog", value=commit_msg, inline=False) + await ctx.send(embed=embed) + + @bot_group.command() + async def repo(self, ctx): + """Display the bot repository""" + await ctx.message.delete() + embed = discord.Embed(title="Code Repository", + description="You can find my source code on [GitDab](" + f"{common.load_config('config.json')[0]['repo_link']}).", + color=common.random_rgb()) + embed.set_footer(text=f"requested by {ctx.author}", icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) + + @bot_group.command() + @commands.is_owner() + async def update(self, ctx): + """Update the bot""" + ctx: commands.Context + await ctx.message.delete() + response = subprocess.run(["git", "pull"], capture_output=True, check=True, + encoding="utf-8").stdout.strip() + embed=discord.Embed(title="Update Report", description=response, color=common.random_rgb()) + embed.set_footer(text=f"requested by {ctx.author}", icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) + + +def setup(bot): + """Initialize the cog""" + bot.add_cog(System(bot)) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/common.py b/core/common.py new file mode 100644 index 0000000..485d523 --- /dev/null +++ b/core/common.py @@ -0,0 +1,49 @@ +import json +from pathlib import Path +from typing import Tuple +import logging +import discord +import random + +logger = logging.getLogger(__name__) + + +def load_config(conf_file) -> Tuple[dict, Path]: + """Load data from a config file.\n + Returns a tuple containing the data as a dict, and the file as a Path""" + config_file = Path(conf_file) + config_file.touch(exist_ok=True) + if config_file.read_text() == "": + config_file.write_text("{}") + logger.debug("config file created.") + with config_file.open("r") as f: + config = json.load(f) + logger.debug("config file loaded.") + return config, config_file + + +def prompt_config(conf_file, msg, key): + """Ensure a value exists in the config file, if it doesn't prompt the bot owner to input via the console.""" + logger.debug(f"checking if {key} is in config.") + config, config_file = load_config(conf_file) + if key not in config: + logger.debug(f"{key} not found in config file.") + config[key] = input(msg) + with config_file.open("w+") as f: + json.dump(config, f, indent=4) + logger.debug(f"'{config[key]}' saved to config file under '{key}'.") + + +def update_config(conf_file, key, value): + logger.debug(f"updating config file '{conf_file}' key '{key}' to '{value}'") + config, config_file = load_config(conf_file) + config[key] = value + with config_file.open("w+") as f: + json.dump(config, f, indent=4) + logger.debug(f"config file '{conf_file}' key '{key}' has been updated to '{value}'") + + +def random_rgb(seed=None): + if seed is not None: + random.seed(seed) + return discord.Colour.from_rgb(random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) diff --git a/main.py b/main.py new file mode 100644 index 0000000..7f637e7 --- /dev/null +++ b/main.py @@ -0,0 +1,37 @@ +#!/usr/bin/env/python +"""Main process for Bot program""" +import subprocess +import logging +import sys +import time +import psutil + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger_handler = logging.StreamHandler() +logger_handler.setFormatter(logging.Formatter('[%(levelname)s] (%(name)s) - %(message)s')) +logger.addHandler(logger_handler) + +logger.info("Bot Manager Started!") + + +def start_bot(): + """Start the bot process""" + bot_process = subprocess.Popen(["python3", "-B", "bot.py"], stdout=sys.stdout) + return bot_process + + +bot = start_bot() +try: + while True: + if bot.poll() is not None: + if bot.returncode == 26: + logger.info("exit code 26 received, restarting bot!") + bot = start_bot() + else: + break + time.sleep(1) # keeps code from overworking. +except KeyboardInterrupt: + print("Killing Bot Process") + psutil.Process(bot.pid).kill() + print("Killed successfully")