generated from InValidFire/discord-bot-template
Initial commit
This commit is contained in:
commit
10532c0c27
10 changed files with 320 additions and 0 deletions
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
MIT License Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
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.
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Discord Bot Template
|
||||||
|
|
||||||
|
This template is used by my Discord Bots to speed up the process of getting started.
|
0
applog/__init__.py
Normal file
0
applog/__init__.py
Normal file
40
applog/logging.yaml
Normal file
40
applog/logging.yaml
Normal file
|
@ -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
|
27
applog/utils.py
Normal file
27
applog/utils.py
Normal file
|
@ -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)
|
37
bot.py
Normal file
37
bot.py
Normal file
|
@ -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'])
|
108
cogs/system.py
Normal file
108
cogs/system.py
Normal file
|
@ -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))
|
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
49
core/common.py
Normal file
49
core/common.py
Normal file
|
@ -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))
|
37
main.py
Normal file
37
main.py
Normal file
|
@ -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")
|
Loading…
Reference in a new issue