Lots of changes

This commit is contained in:
Dustin Pianalto
2019-09-14 11:28:53 -08:00
parent da60570aee
commit a47890f366
25 changed files with 1657 additions and 21 deletions
View File
+168
View File
@@ -0,0 +1,168 @@
import logging
import time
from datetime import datetime
import os
log_format = '{asctime}.{msecs:03.0f}|{levelname:<8}|{name}::{message}'
date_format = '%Y.%m.%d %H.%M.%S'
log_dir = '/tmp/logs/geeksbot'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = '{0}/geeksbot_{1}.log'.format(log_dir, datetime.now().strftime('%Y%m%d_%H%M%S%f'))
ch = logging.StreamHandler()
fh = logging.FileHandler(log_file)
ch.setLevel(logging.INFO)
fh.setLevel(logging.INFO)
formatter = logging.Formatter(log_format, datefmt=date_format, style='{')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=[ch, fh])
logger = logging.getLogger()
logger.info('Logging Setup Complete')
time.sleep(1)
logger.info('Starting Imports')
start = datetime.utcnow()
# noinspection PyPackageRequirements
import discord
logger.info('Discord.py Imported')
# noinspection PyPackageRequirements
from discord.ext import commands
logger.info('commands Imported')
# noinspection PyPackageRequirements
from discord.ext.commands.view import StringView
logger.info('StringView Imported')
# noinspection PyPackageRequirements
from discord.ext.commands.context import Context
logger.info('Context Imported')
logger.info(f'Discord.py Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds')
# noinspection PyRedeclaration
start = datetime.utcnow()
from concurrent import futures
logger.info('Concurrent futures Imported')
from multiprocessing import Pool
logger.info('Multiprocesing Pool Imported')
logger.info(f'Process Libs Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds')
# noinspection PyRedeclaration
start = datetime.utcnow()
import re
logger.info('re Imported')
from typing import Dict
logger.info('Typing Dict Imported')
import json
logger.info('JSON Imported')
import aiohttp
logger.info('aiohttp Imported')
logger.info(f'Misc Libs Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds')
# noinspection PyRedeclaration
# start = datetime.utcnow()
# from geeksbot.imports.rcon_lib import arcon
# from geeksbot.imports import message_logging
# logger.info(f'Geeksbot Libs Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds')
logger.info('Imports Complete')
class Geeksbot(commands.Bot):
def __init__(self, *args, **kwargs):
self.default_prefix = 'g#'
kwargs['command_prefix'] = self.default_prefix
self.description = "Geeksbot v2"
kwargs['description'] = self.description
super().__init__(*args, **kwargs)
self.config_dir = 'geeksbot/config/'
self.config_file = 'bot_config.json'
self.extension_dir = 'exts'
self.aio_session = aiohttp.ClientSession(loop=self.loop)
with open(f'{self.config_dir}{self.config_file}') as f:
self.bot_config = json.load(f)
self.embed_color = discord.Colour.from_rgb(49, 107, 111)
self.error_color = discord.Colour.from_rgb(142, 29, 31)
self.owner_id = 351794468870946827
self.tpe = futures.ThreadPoolExecutor(max_workers=20)
self.process_pool = Pool(processes=4)
self.geo_api = '2d4e419c2be04c8abe91cb5dd1548c72'
self.git_url = 'https://github.com/dustinpianalto/geeksbot_v2'
self.load_default_extensions()
async def load_ext(self, mod):
self.load_extension(f'geeksbot.{self.extension_dir}.{mod}')
logger.info(f'Extension Loaded: {mod}')
async def unload_ext(self, mod):
self.unload_extension(f'geeksbot.{self.extension_dir}.{mod}')
logger.info(f'Extension Loaded: {mod}')
def load_default_extensions(self):
for load_item in self.bot_config['load_list']:
self.loop.create_task(self.load_ext(load_item))
async def close(self):
await super().close()
await self.aio_session.close()
bot = Geeksbot(case_insensitive=True)
@bot.command(hidden=True)
@commands.is_owner()
async def load(ctx, mod=None):
"""Allows the owner to load extensions dynamically"""
await bot.load_ext(mod)
await ctx.send(f'{mod} loaded')
@bot.command(hidden=True)
@commands.is_owner()
async def reload(ctx, mod=None):
"""Allows the owner to reload extensions dynamically"""
if mod == 'all':
load_list = bot.bot_config['load_list']
for load_item in load_list:
await bot.unload_ext(f'{load_item}')
await bot.load_ext(f'{load_item}')
await ctx.send(f'{load_item} reloaded')
else:
await bot.unload_ext(mod)
await bot.load_ext(mod)
await ctx.send(f'{mod} reloaded')
@bot.command(hidden=True)
@commands.is_owner()
async def unload(ctx, mod):
"""Allows the owner to unload extensions dynamically"""
await bot.unload_ext(mod)
await ctx.send(f'{mod} unloaded')
@bot.event
async def on_message(message):
if message.guild:
message.content = message.content.replace('@everyone', '@\uFFF0everyone').replace('@here', '@\uFFF0here')
await bot.process_commands(message)
@bot.event
async def on_ready():
logger.info('Logged in as {0.name}|{0.id}'.format(bot.user))
guild = bot.get_guild(396156980974059531)
channel = guild.get_channel(404569276012560386)
await channel.send('Geeksbot v2 Running')
logger.info('Done loading, Geeksbot is active.')
with open(f'{bot.config_dir}restart') as f:
reboot = f.readlines()
if int(reboot[0]) == 1:
await bot.get_channel(int(reboot[1])).send('Restart Finished.')
with open(f'{bot.config_dir}restart', 'w') as f:
f.write('0')
bot.run(os.environ['TOKEN'])
+5
View File
@@ -0,0 +1,5 @@
{
"load_list": [
"admin"
]
}
+1
View File
@@ -0,0 +1 @@
0
+76
View File
@@ -0,0 +1,76 @@
import discord
from discord.ext import commands
import logging
import inspect
import os
import psutil
import math
from geeksbot.imports import utils
admin_logger = logging.getLogger('admin')
class Admin(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True)
@commands.is_owner()
async def reboot(self, ctx):
await ctx.send('Geeksbot is restarting.')
with open(f'/mnt/{self.bot.SECRETS_BUCKET_NAME}/restart', 'w') as f:
f.write(f'1\n{ctx.channel.id}')
admin_logger.info("Rebooting")
# noinspection PyProtectedMember
os._exit(1)
# TODO Fix view_code
@commands.command(hidden=True)
@commands.is_owner()
async def view_code(self, ctx, code_name):
pag = utils.Paginator(self.bot, prefix='```py', suffix='```')
pag.add(inspect.getsource(self.bot.all_commands[code_name].callback))
book = utils.Book(pag, (None, ctx.channel, ctx.bot, ctx.message))
await book.create_book()
@commands.command(hidden=True)
@commands.is_owner()
async def sysinfo(self, ctx):
"""Gets system status for my server."""
await ctx.send(f'```ml\n'
f'CPU Percentages: {psutil.cpu_percent(percpu=True)}\n'
f'Memory Usage: {psutil.virtual_memory().percent}%\n'
f'Disc Usage: {psutil.disk_usage("/").percent}%\n'
f'```')
@commands.command(aliases=['oauth', 'link'])
@commands.cooldown(1, 5, type=commands.BucketType.user)
async def invite(self, ctx, guy: discord.User = None):
"""Shows you the bot's invite link.
If you pass in an ID of another bot, it gives you the invite link to that bot.
"""
guy = guy or self.bot.user
url = discord.utils.oauth_url(guy.id)
await ctx.send(f'**<{url}>**')
@commands.command()
@commands.cooldown(1, 5, type=commands.BucketType.user)
async def ping(self, ctx):
"""Check the Bot\'s connection to Discord
For more detailed information set the <mode> as comp and it will test the ping
<count> number of times."""
em = discord.Embed(style='rich',
title=f'Pong 🏓',
color=discord.Colour.green()
)
msg = await ctx.send(embed=em)
time1 = ctx.message.created_at
time = (msg.created_at - time1).total_seconds() * 1000
em.description = f'Response Time: **{math.ceil(time)}ms**\n' \
f'Discord Latency: **{math.ceil(self.bot.latency*1000)}ms**'
await msg.edit(embed=em)
def setup(bot):
bot.add_cog(Admin(bot))
View File
+362
View File
@@ -0,0 +1,362 @@
import discord
import asyncio
import typing
# noinspection PyShadowingNames
class Paginator:
def __init__(self,
bot: discord.ext.commands.Bot,
*,
max_chars: int = 1970,
max_lines: int = 20,
prefix: str = '```md',
suffix: str = '```',
page_break: str = '\uFFF8',
field_break: str = '\uFFF7',
field_name_char: str = '\uFFF6',
inline_char: str = '\uFFF5',
max_line_length: int = 100,
embed=False):
_max_len = 6000 if embed else 1980
assert 0 < max_lines <= max_chars
self._parts = list()
self._prefix = prefix
self._suffix = suffix
self._max_chars = max_chars if max_chars + len(prefix) + len(suffix) + 2 <= _max_len \
else _max_len - len(prefix) - len(suffix) - 2
self._max_lines = max_lines - (prefix + suffix).count('\n') + 1
self._page_break = page_break
self._max_line_length = max_line_length
self._pages = list()
self._max_field_chars = 1014
self._max_field_name = 256
self._max_description = 2048
self._embed = embed
self._field_break = field_break
self._field_name_char = field_name_char
self._inline_char = inline_char
self._embed_title = ''
self._embed_description = ''
self._embed_color = None
self._embed_thumbnail = None
self._embed_url = None
self._bot = bot
def set_embed_meta(self, title: str=None,
description: str=None,
color: discord.Colour=None,
thumbnail: str=None,
footer: str='',
url: str=None):
if title and len(title) > self._max_field_name:
raise RuntimeError('Provided Title is too long')
else:
self._embed_title = title
if description and len(description) > self._max_description:
raise RuntimeError('Provided Description is too long')
else:
self._embed_description = description
self._embed_color = color
self._embed_thumbnail = thumbnail
self._embed_url = url
def pages(self) -> typing.List[str]:
_pages = list()
_fields = list()
_page = ''
_lines = 0
_field_name = ''
_field_value = ''
_inline = False
def open_page():
nonlocal _page, _lines, _fields
if not self._embed:
_page = self._prefix
_lines = 0
else:
_fields = list()
def close_page():
nonlocal _page, _lines, _fields
if not self._embed:
_page += self._suffix
_pages.append(_page)
else:
if _fields:
_pages.append(_fields)
open_page()
open_page()
if not self._embed:
for part in [str(p) for p in self._parts]:
if part == self._page_break:
close_page()
new_chars = len(_page) + len(part)
if new_chars > self._max_chars:
close_page()
elif (_lines + (part.count('\n') + 1 or 1)) > self._max_lines:
close_page()
_lines += (part.count('\n') + 1 or 1)
_page += '\n' + part
else:
def open_field(name: str):
nonlocal _field_value, _field_name
_field_name = name
_field_value = self._prefix
def close_field(next_name: str=None):
nonlocal _field_name, _field_value, _fields
_field_value += self._suffix
if _field_value != self._prefix + self._suffix:
_fields.append({'name': _field_name, 'value': _field_value, 'inline': _inline})
if next_name:
open_field(next_name)
open_field('\uFFF0')
for part in [str(p) for p in self._parts]:
if part.strip() == self._page_break:
close_page()
continue
elif part == self._field_break:
if len(_fields) + 1 < 25:
close_field(next_name='\uFFF0')
else:
close_field()
close_page()
continue
if part.startswith(self._field_name_char):
part = part.replace(self._field_name_char, '')
if part.startswith(self._inline_char):
_inline = True
part = part.replace(self._inline_char, '')
else:
_inline = False
if _field_value and _field_value != self._prefix:
close_field(part)
else:
_field_name = part
continue
_field_value += '\n' + part
close_field()
close_page()
self._pages = _pages
return _pages
# noinspection PyUnresolvedReferences
def process_pages(self) -> typing.List[str]:
_pages = self._pages or self.pages()
_len_pages = len(_pages)
_len_page_str = len(f'{_len_pages}/{_len_pages}')
if not self._embed:
for i, page in enumerate(_pages):
if len(page) + _len_page_str <= 2000:
_pages[i] = f'{i + 1}/{_len_pages}\n{page}'
else:
for i, page in enumerate(_pages):
em = discord.Embed(title=self._embed_title,
description=self._embed_description,
color=self._bot.embed_color,
)
if self._embed_thumbnail:
em.set_thumbnail(url=self._embed_thumbnail)
if self._embed_url:
em.url = self._embed_url
if self._embed_color:
em.color = self._embed_color
em.set_footer(text=f'{i + 1}/{_len_pages}')
for field in page:
em.add_field(name=field['name'], value=field['value'], inline=field['inline'])
_pages[i] = em
return _pages
def __len__(self):
return sum(len(p) for p in self._parts)
def __eq__(self, other):
# noinspection PyProtectedMember
return self.__class__ == other.__class__ and self._parts == other._parts
def add_page_break(self, *, to_beginning: bool=False) -> None:
self.add(self._page_break, to_beginning=to_beginning)
def add(self, item: typing.Any, *, to_beginning: bool=False, keep_intact: bool=False, truncate=False) -> None:
item = str(item)
i = 0
if not keep_intact and not item == self._page_break:
item_parts = item.strip('\n').split('\n')
for part in item_parts:
if len(part) > self._max_line_length:
if not truncate:
length = 0
out_str = ''
def close_line(line):
nonlocal i, out_str, length
self._parts.insert(i, out_str) if to_beginning else self._parts.append(out_str)
i += 1
out_str = line + ' '
length = len(out_str)
bits = part.split(' ')
for bit in bits:
next_len = length + len(bit) + 1
if next_len <= self._max_line_length:
out_str += bit + ' '
length = next_len
elif len(bit) > self._max_line_length:
if out_str:
close_line(line='')
for out_str in [bit[i:i + self._max_line_length]
for i in range(0, len(bit), self._max_line_length)]:
close_line('')
else:
close_line(bit)
close_line('')
else:
line = f'{part:.{self._max_line_length-3}}...'
self._parts.insert(i, line) if to_beginning else self._parts.append(line)
else:
self._parts.insert(i, part) if to_beginning else self._parts.append(part)
i += 1
elif keep_intact and not item == self._page_break:
if len(item) >= self._max_chars or item.count('\n') > self._max_lines:
raise RuntimeError('{item} is too long to keep on a single page and is marked to keep intact.')
if to_beginning:
self._parts.insert(0, item)
else:
self._parts.append(item)
else:
if to_beginning:
self._parts.insert(0, item)
else:
self._parts.append(item)
class Book:
def __init__(self, pag: Paginator, ctx: typing.Tuple[typing.Optional[discord.Message],
discord.TextChannel,
discord.ext.commands.Bot,
discord.Message]) -> None:
self._pages = pag.process_pages()
self._len_pages = len(self._pages)
self._current_page = 0
self._message, self._channel, self._bot, self._calling_message = ctx
self._locked = True
if pag == Paginator(self._bot):
raise RuntimeError('Cannot create a book out of an empty Paginator.')
def advance_page(self) -> None:
self._current_page += 1
if self._current_page >= self._len_pages:
self._current_page = 0
def reverse_page(self) -> None:
self._current_page += -1
if self._current_page < 0:
self._current_page = self._len_pages - 1
async def display_page(self) -> None:
if isinstance(self._pages[self._current_page], discord.Embed):
if self._message:
await self._message.edit(content=None, embed=self._pages[self._current_page])
else:
self._message = await self._channel.send(embed=self._pages[self._current_page])
else:
if self._message:
await self._message.edit(content=self._pages[self._current_page], embed=None)
else:
self._message = await self._channel.send(self._pages[self._current_page])
async def create_book(self) -> None:
# noinspection PyUnresolvedReferences
async def reaction_checker():
# noinspection PyShadowingNames
def check(reaction, user):
if self._locked:
return str(reaction.emoji) in self._bot.book_emojis.values() \
and user == self._calling_message.author \
and reaction.message.id == self._message.id
else:
return str(reaction.emoji) in self._bot.book_emojis.values() \
and reaction.message.id == self._message.id
await self.display_page()
if len(self._pages) > 1:
for emoji in self._bot.book_emojis.values():
try:
await self._message.add_reaction(emoji)
except (discord.Forbidden, KeyError):
pass
else:
try:
await self._message.add_reaction(self._bot.book_emojis['unlock'])
await self._message.add_reaction(self._bot.book_emojis['close'])
except (discord.Forbidden, KeyError):
pass
while True:
try:
reaction, user = await self._bot.wait_for('reaction_add', timeout=60, check=check)
except asyncio.TimeoutError:
try:
await self._message.clear_reactions()
except (discord.Forbidden, discord.NotFound):
pass
raise asyncio.CancelledError
else:
await self._message.remove_reaction(reaction, user)
if str(reaction.emoji) == self._bot.book_emojis['close']:
await self._calling_message.delete()
await self._message.delete()
raise asyncio.CancelledError
elif str(reaction.emoji) == self._bot.book_emojis['forward']:
self.advance_page()
elif str(reaction.emoji) == self._bot.book_emojis['back']:
self.reverse_page()
elif str(reaction.emoji) == self._bot.book_emojis['end']:
self._current_page = self._len_pages - 1
elif str(reaction.emoji) == self._bot.book_emojis['start']:
self._current_page = 0
elif str(reaction.emoji) == self._bot.book_emojis['hash']:
m = await self._channel.send(f'Please enter a number in range 1 to {self._len_pages}')
def num_check(message):
if self._locked:
return message.content.isdigit() \
and 0 < int(message.content) <= self._len_pages \
and message.author == self._calling_message.author
else:
return message.content.isdigit() \
and 0 < int(message.content) <= self._len_pages
try:
msg = await self._bot.wait_for('message', timeout=30, check=num_check)
except asyncio.TimeoutError:
await m.edit(content='Message Timed out.')
else:
self._current_page = int(msg.content) - 1
try:
await m.delete()
await msg.delete()
except (discord.Forbidden, discord.NotFound):
pass
elif str(reaction.emoji) == self._bot.book_emojis['unlock']:
self._locked = False
await self._message.remove_reaction(reaction, self._channel.guild.me)
continue
await self.display_page()
self._bot.loop.create_task(reaction_checker())