Updates to stuff lol

This commit is contained in:
Dustin Pianalto 2019-09-25 21:39:04 -08:00
parent b3eefa1266
commit 27231fda7b
39 changed files with 1974 additions and 154 deletions

View File

@ -57,6 +57,8 @@ import json # noqa: E402
logger.info('JSON Imported') logger.info('JSON Imported')
import aiohttp # noqa: E402 import aiohttp # noqa: E402
logger.info('aiohttp Imported') logger.info('aiohttp Imported')
import redis # noqa: E402
logger.info('redis Imported')
logger.info(f'Misc Libs Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds') logger.info(f'Misc Libs Import Complete - Took {(datetime.utcnow() - start).total_seconds()} seconds')
# noinspection PyRedeclaration # noinspection PyRedeclaration
# start = datetime.utcnow() # start = datetime.utcnow()
@ -78,8 +80,11 @@ class Geeksbot(commands.Bot):
self.config_dir = 'geeksbot/config/' self.config_dir = 'geeksbot/config/'
self.config_file = 'bot_config.json' self.config_file = 'bot_config.json'
self.extension_dir = 'exts' self.extension_dir = 'exts'
self.cache = redis.Redis(host=os.environ['REDIS_HOST'], port=os.environ['REDIS_PORT'], db=1, charset="utf-8", decode_responses=True)
self.api_token = os.environ['API_TOKEN'] self.api_token = os.environ['API_TOKEN']
self.aio_session = aiohttp.ClientSession(loop=self.loop) self.aio_session = aiohttp.ClientSession(loop=self.loop)
self.auth_header = {'Authorization': f'Token {self.api_token}'}
self.api_base = 'https://geeksbot.app/api'
with open(f'{self.config_dir}{self.config_file}') as f: with open(f'{self.config_dir}{self.config_file}') as f:
self.bot_config = json.load(f) self.bot_config = json.load(f)
self.embed_color = discord.Colour.from_rgb(49, 107, 111) self.embed_color = discord.Colour.from_rgb(49, 107, 111)

View File

@ -1,5 +1,7 @@
{ {
"load_list": [ "load_list": [
"admin" "admin",
"exec",
"message_events"
] ]
} }

View File

@ -0,0 +1,99 @@
import discord
from discord.ext import commands
from datetime import datetime
import logging
message_logger = logging.getLogger('MessageEvents')
class MessageEvents(commands.Cog):
def __init__(self, bot):
self.bot = bot
def get_message_data(self, message: discord.Message):
data = {
'id': message.id,
'author': message.author.id,
'guild': message.guild.id,
'channel': message.channel.id,
'created_at': message.created_at.timestamp(),
'tagged_everyone': message.mention_everyone,
'content': message.content,
'embeds': [e.to_dict() for e in message.embeds],
'tagged_users': [user.id for user in message.mentions],
'tagged_roles': [role.id for role in message.role_mentions],
'tagged_channels': [channel.id for channel in message.channel_mentions],
}
return data
@commands.Cog.listener(name='on_message')
async def on_message(self, message):
message_logger.info(f'Got Message')
r = await self.bot.aio_session.get(f'{self.bot.api_base}/users/{message.author.id}/',
headers=self.bot.auth_header)
if r.status != 200:
message_logger.warning(f'User not found: {message.author.id} Status: {r.status}')
return
user = await r.json()
if user.get('logging_enabled'):
message_data = self.get_message_data(message)
r = await self.bot.aio_session.post(f'{self.bot.api_base}/messages/',
headers=self.bot.auth_header,
json=message_data)
message_logger.info(f'Storing Message:\nStatus: {r.status}\n{await r.json()}')
@commands.Cog.listener()
async def on_raw_message_delete(self, payload):
data = {
'deleted_at': datetime.utcnow().timestamp()
}
r = await self.bot.aio_session.put(f'{self.bot.api_base}/messages/{payload.message_id}',
headers=self.bot.auth_header,
json=data)
message_logger.info(f'Deleting Message {payload.message_id}:\nStatus: {r.status}\n{await r.json()}')
@commands.Cog.listener()
async def on_raw_bulk_message_delete(self, payload):
data = {
'deleted_at': datetime.utcnow().timestamp()
}
for id in payload.message_ids:
r = await self.bot.aio_session.put(f'{self.bot.api_base}/messages/{id}',
headers=self.bot.auth_header,
json=data)
message_logger.info(f'Deleting Message {id}:\nStatus: {r.status}\n{await r.json()}')
@commands.Cog.listener()
async def on_raw_message_edit(self, payload):
if payload.data.get('mentions'):
tagged_users = [user.id for user in payload.data.get('mentions')]
else:
tagged_users = None
if payload.data.get('mention_roles'):
tagged_roles = [role.id for role in payload.data.get('mention_roles')]
else:
tagged_roles = None
if payload.data.get('mention_channels'):
tagged_channels = [channel.id for channel in payload.data.get('mention_channels')]
else:
tagged_channels = None
data = {
'modified_at': datetime.utcnow().timestamp(),
'content': payload.data.get('content'),
'embeds': payload.data.get('embeds'),
'tagged_everyone': payload.data.get('mention_everyone'),
'tagged_users': tagged_users,
'tagged_roles': tagged_roles,
'tagged_channels': tagged_channels
}
r = await self.bot.aio_session.put(f'{self.bot.api_base}/messages/{payload.message_id}/',
headers=self.bot.auth_header,
json=data)
message_logger.info(f'Editing Message {payload.message_id}\nStatus: {r.status}\n{await r.json()}')
def setup(bot):
bot.add_cog(MessageEvents(bot))

948
geeksbot/exts/rcon.py Normal file
View File

@ -0,0 +1,948 @@
import discord
from discord.ext.commands import Cog
import logging
from datetime import datetime
import asyncio
import typing
from geeksbot.imports import arcon
from geeksbot.imports import utils
from geeksbot.imports import checks
rcon_log = logging.getLogger('rcon')
class Rcon(Cog):
def __init__(self, bot):
self.bot = bot
async def connect_rcon_server(self, *, name: str, ip: str, port: int, password: str,
single_packet: bool = False, force: bool = False, monitor_chat: bool = False,
server_chat_channel: int = None, server_messages_channel: int = None) \
-> typing.Union[int, str]:
if name.replace('_', ' ').title() in self.bot.connected_ark_servers.keys() \
and isinstance(self.bot.connected_ark_servers[name], arcon.ARKServer) \
and self.bot.connected_ark_servers[name].writer \
and not force:
rcon_log.info(f"{name.replace('_', ' ').title()} is already connected.")
return 1
server = arcon.ARKServer(host=ip, port=port, password=password, single_packet=single_packet,
monitor_chat=monitor_chat, server_chat_channel=server_chat_channel,
server_messages_channel=server_messages_channel)
rcon_log.info(f'{name.replace("_", " ").title()} configured.')
connected = await server.connect()
if connected == 1:
rcon_log.info(f'{name.replace("_", " ").title()} connected.')
self.bot.connected_ark_servers[name.replace('_', ' ').title()] = server
return 1
elif connected == -1:
rcon_log.info(f'{name.replace("_", " ").title()} timeout error.')
return 'Timeout Error'
elif connected == 0:
rcon_log.info(f'{name.replace("_", " ").title()} auth error.')
return 'Authentication Failed'
else:
rcon_log.info(f'{name.replace("_", " ").title()} unknown error.')
return 'Unknown Error'
async def get_rcon_server_by_name(self, *, guild_config: dict, name: str) \
-> typing.Union[dict, arcon.ARKServer, None]:
guild_servers = guild_config.get('rcon_connections')
if guild_servers:
if name == '*':
server_list = {}
for server, info in guild_servers.items():
if server == name.replace('_', ' ').title():
if server not in self.bot.connected_ark_servers.keys():
connection_result = await self.connect_rcon_server(
name=server,
ip=info['ip'],
port=info['port'],
password=info['password'],
monitor_chat=info.get('monitoring_chat', None),
server_chat_channel=info.get('game_chat_chan_id', None),
server_messages_channel=info.get('msg_chan_id', None)
)
if connection_result != 1:
return None
return self.bot.connected_ark_servers[server]
elif name == '*':
if server not in self.bot.connected_ark_servers.keys():
connection_result = await self.connect_rcon_server(
name=server,
ip=info['ip'],
port=info['port'],
password=info['password'],
monitor_chat=info.get('monitoring_chat', None),
server_chat_channel=info.get('game_chat_chan_id', None),
server_messages_channel=info.get('msg_chan_id', None)
)
if connection_result != 1:
# noinspection PyUnboundLocalVariable
server_list[server] = f'Cannot connect to the {server} server.\n{connection_result}'
try:
# noinspection PyUnboundLocalVariable
server_list[server] = self.bot.connected_ark_servers[server]
except KeyError:
rcon_log.warning(server_list[server])
del server_list[server]
else:
if name == '*':
return server_list
return None
else:
return None
@staticmethod
async def admin(bot, guild, msg, server_name, server_con: arcon.ARKServer, guild_config: dict):
player = msg.split(' ||| ')[1].split(' (')[0]
rcon_log.info(f'{player} requested admin assistance')
admin_roles = guild_config.get('admin_roles')
if admin_roles:
for role in admin_roles:
msg = '{0} {1}'.format(msg, discord.utils.get(guild.roles, id=admin_roles[role]).mention)
await server_con.server_chat_to_player_name(player, 'GeeksBot: Admin Geeks have been notified you need '
'assistance. Please be patient.')
return msg
async def dinowipe(self, bot, guild, msg, server_name, server_con: arcon.ARKServer, guild_config: dict):
player = msg.split(' ||| ')[1].split(' (')[0]
steam_ref = self.bot.fs_db.collection(f'users').where('steam_name', '==', player)
user_info = await self.bot.loop.run_in_executor(self.bot.tpe, steam_ref.get)
steamid = None
user = None
for user in user_info:
if user:
steamid = user.to_dict().get('steam_id')
break
if steamid:
if not self.bot.dino_wipe_request.get(server_name):
if user:
player = await patron.Patron.from_id(bot, steamid, discord_id=int(user.id))
rcon_log.info(f'{player.steam_name} requested a wild dino wipe')
member = guild.get_member(player.discord_id)
self.bot.loop.create_task(self.request_dinowipe(member, server_name, server_con))
else:
await server_con.server_chat_to_player_name(player,
'Sorry, an error has occurred please try again.')
else:
await server_con.server_chat_to_player_name(player, 'A Wild Dino Wipe request is already in progress.')
else:
await server_con.server_chat_to_player_name(player, 'Sorry. You are not registered to run commands via the '
'in-game chat. Please send a chat message in-game '
'$register steamid=your_steam_id and follow the '
'instructions so I can link your character with your '
'Discord account. Thanks')
return msg
@staticmethod
async def delaywipe(bot, guild: discord.Guild, msg, server_name, server_con: arcon.ARKServer, guild_config: dict):
player = msg.split(' ||| ')[1].split(' (')[0]
if bot.dino_wipe_request.get(server_name):
bot.dino_wipe_request[server_name] = False
await server_con.broadcast(f'Wild Dino Wipe has been delayed by {player}. '
f'Please watch chat for timing updates')
elif bot.dino_wipe_request.get(server_name) is None:
await server_con.server_chat_to_player_name(player, 'There are no Wild Dino Wipes pending.')
else:
await server_con.server_chat_to_player_name(player, 'The dino wipe has already been delayed.')
return msg
@staticmethod
async def register(bot, guild, msg, server_name, server_con: arcon.ARKServer, guild_config: dict):
player = msg.split(' ||| ')[1].split(' (')[0]
steamid = msg.split(' ||| ')[1].split('steamid=')[1].split(' ')[0].replace('`', '').replace("'", '').strip()
rcon_log.info(f'{player} - {steamid}')
try:
int(steamid)
except ValueError:
await server_con.server_chat_to_player_name(player, 'That is not a valid SteamID')
return msg
else:
identifier = randint(10000, 99999)
prefix = guild_config.get("prefixes", bot.default_prefix)
await server_con.server_chat_to_player_name(player,
'Your SteamID has been noted, to finish registering '
'please run the following command in the discord '
f'channel. You will not be able to run commands '
f'in-game until your registration is completed... '
f'{prefix[0] if isinstance(prefix, list) else prefix}'
f'register {identifier}')
bot.pending_registrations[identifier] = (player, steamid)
return msg
@commands.command(name='register')
async def register_discord_account(self, ctx, identifier: int=None):
user_ref = self.bot.fs_db.document(f'users/{ctx.author.id}')
user_info = (await self.bot.loop.run_in_executor(self.bot.tpe, user_ref.get)).to_dict()
if user_info.get('steam_id') and user_info.get('steam_name'):
await ctx.send('You are already registered to run commands in-game')
return
if identifier not in self.bot.pending_registrations.keys():
await ctx.send('That identifier is not in the pending registrations')
return
steam_info = self.bot.pending_registrations[identifier]
await self.bot.loop.run_in_executor(self.bot.tpe, user_ref.update, {
'steam_name': steam_info[0],
'steam_id': steam_info[1]
})
await ctx.send('Registration complete. You are now authorized to run commands in-game on the ARK servers.')
del self.bot.pending_registrations[identifier]
async def create_server_chat_chan(self, guild: discord.Guild, server_name: str,
server_con: arcon.ARKServer, guild_config: dict):
rcon_log.info(f'Creating channel for {server_name}')
category = discord.utils.get(guild.categories, name='Server Chats')
if category is None:
overrides = {guild.default_role: discord.PermissionOverwrite(read_messages=False)}
category = await guild.create_category('Server Chats', overwrites=overrides)
rcon_log.info(category)
channels = guild.channels
if category:
for channel in category.channels:
if server_con.server_chat_channel == channel.id:
return channel
else:
rcon_log.info(f'Creating {server_name}')
chan = await guild.create_text_channel(f'{server_name}', category=category)
server_con.server_chat_channel = chan.id
if guild_config.get('rcon_connections'):
guild_config['rcon_connections'][server_name]['game_chat_chan_id'] = chan.id
guild_ref = self.bot.fs_db.document(f'guilds/{guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, guild_config)
return chan
async def _monitor_chat(self, guild, server_name, server_con, guild_config: dict):
# noinspection PyShadowingNames
async def start_monitor_chat(bot, guild, *, server_name: str,
server_con: arcon.ARKServer, guild_config: dict):
while server_con.monitor_chat:
messages = await server_con.getchat()
rcon_log.debug('Got chat from {0}.'.format(server_name))
for message in [msg.strip() for msg in messages.split('\n')
if msg.strip() != 'Server received, But no response!!']:
rcon_log.info(message)
message_out = f'```{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ||| {message}```'
for command in self.bot.game_commands:
prefix_command = '{0}{1}'.format(self.bot.game_prefix, command)
if message.split('): ')[-1].startswith(prefix_command):
try:
func = getattr(self, command)
except AttributeError:
rcon_log.warning('Function not found "{0}"'.format(command))
else:
rcon_log.info(f'Sending to {command}')
message_out = await func(bot, guild, message_out, server_name, server_con, guild_config)
await guild.get_channel(server_con.server_chat_channel).send(message_out)
await asyncio.sleep(1)
await guild.get_channel(server_con.server_chat_channel).send('Monitoring Stopped')
rcon_log.info(server_con.server_chat_channel)
if server_con.server_chat_channel:
channel = self.bot.get_channel(server_con.server_chat_channel)
if not channel:
channel = await self.create_server_chat_chan(guild, server_name, server_con, guild_config)
else:
channel = await self.create_server_chat_chan(guild, server_name, server_con, guild_config)
rcon_log.info(channel)
self.bot.loop.create_task(start_monitor_chat(self.bot, guild, server_name=server_name,
server_con=server_con, guild_config=guild_config))
await channel.send('Started monitoring on the {0} server.'.format(server_name))
rcon_log.debug('Started monitoring on the {0} server.'.format(server_name))
@commands.command()
@commands.guild_only()
async def monitor_chat(self, ctx, *, server=None):
"""Begins monitoring the specified ARK server for chat messages and other events.
The specified server must already be in the current guild\'s configuration.
To add and remove ARK servers from the guild see add_rcon_server and remove_rcon_server.
The server argument is not case sensitive and if the server name has two
words it can be in one of the following forms:
first last
first_last
"first last"
To view all the valid ARK servers for this guild see list_ark_servers."""
if await checks.is_rcon_admin(self.bot, ctx):
if server is not None:
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name=server)
if server_con:
server_con.monitor_chat = True
ctx.guild_config['rcon_connections'][server]["monitoring_chat"] = 1
guild_ref = self.bot.fs_db.document(f'guilds/{ctx.guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, ctx.guild_config)
await self._monitor_chat(ctx.guild, server, server_con, ctx.guild_config)
await ctx.message.add_reaction('')
else:
await ctx.send(f'Server not found: {server}')
else:
await ctx.send(f'You must include a server in this command.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command()
@commands.guild_only()
async def end_monitor_chat(self, ctx, *, server=None):
"""Ends chat monitoring on the specified server.
Context is the same as monitor_chat"""
if await checks.is_rcon_admin(self.bot, ctx):
if server is not None:
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name=server)
if server_con:
server_con.monitor_chat = False
else:
await ctx.send(f'{server} is not connected currently.')
if server in ctx.guild_config.get('rcon_connections', []):
ctx.guild_config['rcon_connections'][server]["monitoring_chat"] = 0
guild_ref = self.bot.fs_db.document(f'guilds/{ctx.guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, ctx.guild_config)
else:
await ctx.send(f'Server not found in config: {server}')
else:
await ctx.send(f'You must include a server in this command.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command()
@commands.guild_only()
async def listplayers(self, ctx, *, server_name=None):
"""Lists the players currently connected to the specified ARK server.
The specified server must already be in the current guild\'s configuration.
To add and remove ARK servers from the guild see add_rcon_server and remove_rcon_server.
The server argument is not case sensitive and if the server name has two
words it can be in one of the following forms:
first last
first_last
"first last"
To view all the valid ARK servers for this guild see list_ark_servers."""
if await checks.is_rcon_admin(self.bot, ctx):
if server_name:
server_name = server_name.replace('_', ' ').title()
server: arcon.ARKServer = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name=server_name)
if server:
msg = await ctx.send(f'**Getting Data for the {server_name} server**')
await ctx.channel.trigger_typing()
message = await server.listplayers()
await ctx.channel.trigger_typing()
await msg.delete()
await ctx.send(f'**Players currently on the {server_name} server:**\n{message}')
else:
await ctx.send(f'That server is not in my configuration.\nPlease add it via !add_rcon_server '
f'"{server_name}" "ip" port "password" if you would like to get info from it.')
else:
futures = []
guild_servers: dict = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name='*'
)
if guild_servers is not None:
for name, server in guild_servers.items():
if not isinstance(server, arcon.ARKServer):
await ctx.send(f'__**{server}**__')
else:
msg = await ctx.send(f'**Getting Data for the {name} server**')
# noinspection PyShadowingNames
async def _listplayers(*, name, server_con: arcon.ARKServer, mess: discord.Message):
name = name.replace('_', ' ').title()
response = await server_con.listplayers()
await mess.edit(content=f'**Players currently on the {name} server:**\n{response}')
futures.append(_listplayers(mess=msg, server_con=server, name=name))
if futures:
asyncio.ensure_future(asyncio.gather(*futures))
else:
await ctx.send('There are no available servers for this guild.')
else:
await ctx.send('There are no available servers for this guild.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command()
@commands.guild_only()
async def add_rcon_server(self, ctx, server, ip, port, password):
"""Adds the specified server to the current guild\'s rcon config.
All multi-word strings (<server>, <ip>, <password>) must be contained inside double quotes."""
if await checks.is_rcon_admin(self.bot, ctx):
server = server.replace('_', ' ').title()
if ctx.guild_config.get('rcon_connections'):
if server not in ctx.guild_config['rcon_connections']:
ctx.guild_config['rcon_connections'][server] = {
'ip': ip,
'port': port,
'password': password,
'game_chat_chan_id': 0,
'msg_chan_id': 0,
'monitoring_chat': 0
}
guild_ref = self.bot.fs_db.document(f'guilds/{ctx.guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, ctx.guild_config)
await ctx.send('{0} server has been added to my configuration.'.format(server))
else:
await ctx.send('This server name is already in my configuration. Please choose another.')
else:
ctx.guild_config['rcon_connections'] = {server: {
'ip': ip,
'port': port,
'password': password,
'game_chat_chan_id': 0,
'msg_chan_id': 0,
'monitoring_chat': 0
}}
guild_ref = self.bot.fs_db.document(f'guilds/{ctx.guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, ctx.guild_config)
await ctx.send('{0} server has been added to my configuration.'.format(server))
else:
await ctx.send(f'You are not authorized to run this command.')
await ctx.message.delete()
await ctx.send('Command deleted to prevent password leak.')
@commands.command()
@commands.guild_only()
async def remove_rcon_server(self, ctx, *, server: str):
"""removes the specified server from the current guild\'s rcon config."""
if await checks.is_rcon_admin(self.bot, ctx):
server = server.replace('_', ' ').title()
if server in ctx.guild_config.get('rcon_connections', []):
del ctx.guild_config['rcon_connections'][server]
guild_ref = self.bot.fs_db.document(f'guilds/{ctx.guild.id}')
await self.bot.loop.run_in_executor(self.bot.tpe, guild_ref.update, ctx.guild_config)
await ctx.send('{0} has been removed from my configuration.'.format(server))
else:
await ctx.send('{0} is not in my configuration.'.format(server))
else:
await ctx.send(f'You are not authorized to run this command.')
@staticmethod
async def _whitelist(*, server_name: str, server_con: arcon.ARKServer,
player: patron.Patron, message: discord.Message, message_lock: asyncio.Lock):
result = await server_con.whitelist(player.steam_id)
if result == f'{player.steam_id} Allow Player To Join No Check':
with await message_lock:
message = await message.channel.get_message(message.id)
await message.edit(content=f'{message.content}\n{server_name.replace("_", " ").title()}'
f' Done!')
else:
with await message_lock:
message = await message.channel.get_message(message.id)
await message.edit(content=f'{message.content}\n{server_name.replace("_", " ").title()}'
f' Failed!')
@commands.command(name='add_whitelist')
@commands.guild_only()
async def add_whitelist(self, ctx, *, members: str=None):
if await checks.is_rcon_admin(self.bot, ctx):
if members:
futures = []
members = members.replace(', ', '').split(',')
converter = commands.MemberConverter()
for member in members:
try:
member = await converter.convert(ctx, member)
except commands.errors.BadArgument:
try:
member = int(member)
except ValueError:
raise ValueError(f'Member {member} can\'t be found and is not a valid Steam64 ID.')
if isinstance(member, discord.Member):
player = await patron.Patron.from_name(self.bot, discord_name=member)
else:
player = await patron.Patron.from_id(self.bot, steam_id=member)
if player == -1:
await ctx.send(f'{ctx.author.mention} I Cannot find a player with a discord name/steam id of '
f'{member} in the current whitelist sheet. Did you forget to '
f'move them to the correct sheet?')
else:
rcon_connections: dict = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name='*')
if rcon_connections:
msg = await ctx.send(f'**Whitelisting {player.discord_name} on all servers**')
lock = asyncio.Lock()
for server_name, server_con in rcon_connections.items():
futures.append(self._whitelist(server_name=server_name, server_con=server_con,
player=player, message=msg, message_lock=lock))
if futures:
asyncio.ensure_future(asyncio.gather(*futures), loop=self.bot.loop)
else:
await ctx.send('Nothing for me to do')
return
else:
await ctx.send('I need a list of members to whitelist.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command(name='new_patron')
@commands.guild_only()
async def register_new_patron(self, ctx, *, members: str=None):
"""Adds the included Steam 64 IDs to the running whitelist on all the ARK servers in the current guild\'s rcon config.
Steam 64 IDs should be a comma separated list of IDs.
Example: 76561198024193239,76561198024193239,76561198024193239"""
if await checks.is_rcon_admin(self.bot, ctx):
if members is not None:
async with ctx.typing():
members = members.replace(', ', ',').split(',')
converter = commands.MemberConverter()
members = [await converter.convert(ctx, m) for m in members]
futures = []
patrons = []
rcon_connections: dict = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name='*')
if not isinstance(members, list):
members = [members, ]
for member in members:
member: discord.Member
player = await patron.Patron.from_name(self.bot, discord_name=member)
if player == -1:
await ctx.send(f'{ctx.author.mention} I Cannot find a player with a discord name of '
f'{member.display_name} in the current whitelist sheet. Did you forget to '
f'move them to the correct sheet?')
else:
if rcon_connections:
msg = await ctx.send(f'**Whitelisting {player.discord_name} on all servers**')
lock = asyncio.Lock()
for server_name, server_con in rcon_connections.items():
futures.append(self._whitelist(server_name=server_name, server_con=server_con,
player=player, message=msg, message_lock=lock))
roles = []
patron_roles = ctx.guild_config.get('patreon_tiers')
creator_roles = ctx.guild_config.get('patreon_creators')
if creator_roles:
rcon_log.info(f'patron_of {player.patron_of}')
if 'both' in player.patron_of.casefold():
rcon_log.info('found both')
for role_id in creator_roles.values():
roles.append(ctx.guild.get_role(int(role_id)))
else:
roles.append(ctx.guild.get_role(
int(creator_roles[player.patron_of + '_Patron'])
))
if patron_roles:
roles.append(ctx.guild.get_role(
int(patron_roles[player.patreon_tier.strip().title()])
))
if roles:
role_str = '\n'.join([role.name for role in roles])
await ctx.send(f'Adding {player.discord_name} to the following roles:\n'
f'{role_str}')
await ctx.guild.get_member(int(player.discord_id)).add_roles(*roles)
patrons.append(player)
if futures:
asyncio.ensure_future(asyncio.gather(*futures), loop=self.bot.loop)
else:
await ctx.send('Nothing for me to do')
return
if patrons:
new_patron_message = ctx.guild_config.get('new_patron_message')
new_patron_channel = ctx.guild_config.get('new_patron_channel')
if new_patron_message and new_patron_channel:
channel = ctx.guild.get_channel(int(new_patron_channel))
if channel:
patron_mentions = []
for p in patrons:
member = ctx.guild.get_member(int(p.discord_id))
patron_mentions.append(member.mention)
prelude = new_patron_message.get('prelude')
message = ''
if prelude:
if len(patron_mentions) > 1:
message = prelude.get('plural')
else:
message = prelude.get('singular')
body = new_patron_message.get('body')
if body:
server_info_channel = ctx.guild.get_channel(int(
ctx.guild_config.get('server_info')
))
message += ' ' + body.format(users=', '.join(patron_mentions),
server_info=server_info_channel.mention if
server_info_channel else "server_info")
await channel.send(message)
else:
await ctx.send('I need a list of members to add roles to and whitelist.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command()
@commands.guild_only()
async def saveworld(self, ctx, *, server=None):
"""Runs SaveWorld on the specified ARK server.
If a server is not specified it will default to running saveworld on all servers in the guild\'s config.
Will print out "World Saved" for each server when the command completes successfully."""
if await checks.is_rcon_admin(self.bot, ctx):
# noinspection PyShadowingNames
async def _saveworld(ctx, server_name: str, server_con: arcon.ARKServer):
response = await server_con.saveworld()
if response == 'World Saved':
await ctx.send(f'{server_name} Saved')
else:
await ctx.send(f'Failed to save {server_name}')
futures = []
async with ctx.typing():
if server is None:
rcon_connections: dict = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name='*')
if rcon_connections:
for server_name, server_con in rcon_connections.items():
futures.append(_saveworld(ctx, server_name, server_con))
self.bot.loop.create_task(asyncio.gather(*futures))
else:
await ctx.send('There are no available servers for this guild.')
else:
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name=server
)
if server_con:
# noinspection PyTypeChecker
await _saveworld(ctx, server, server_con)
else:
await ctx.send(f'{server} is not currently in the configuration for this guild.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.group(case_insensitive=True)
async def broadcast(self, ctx):
"""Run help broadcast for more info"""
pass
@broadcast.command(name='all', aliases=['a'])
@commands.guild_only()
async def broadcast_all(self, ctx, *, message=None):
"""Sends a broadcast message to all servers in the guild config.
The message will be prefixed with the Discord name of the person running the command.
Will print "Success" for each server once the broadcast is sent."""
if await checks.is_rcon_admin(self.bot, ctx):
if message is not None:
# noinspection PyShadowingNames
async def _broadcast(*, message: str, server_con: arcon.ARKServer, server_name: str,
msg: discord.Message, message_lock: asyncio.Lock):
print(server_con.host, server_con.port)
response = await server_con.broadcast(message)
if response == 'Server received, But no response!!':
with await message_lock:
msg = await msg.channel.get_message(msg.id)
await msg.edit(content=f'{msg.content}\n{server_name} Success')
else:
with await message_lock:
msg = await msg.channel.get_message(msg.id)
await msg.edit(content=f'{msg.content}\n{server_name} Failed')
futures = []
rcon_connections: dict = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name='*')
if rcon_connections:
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
msg = await ctx.send(f'Broadcasting "{message}" to all servers.')
lock = asyncio.Lock()
for server_name, server_con in rcon_connections.items():
futures.append(_broadcast(message=message, server_con=server_con, server_name=server_name,
msg=msg, message_lock=lock))
self.bot.loop.create_task(asyncio.gather(*futures))
await ctx.message.add_reaction('')
else:
await ctx.send('There are no available servers for this guild.')
else:
await ctx.send('You must include a message with this command.')
else:
await ctx.send(f'You are not authorized to run this command.')
@broadcast.command(name='server')
@commands.guild_only()
async def broadcast_server(self, ctx, server, *, message=None):
"""Sends a broadcast message to the specified server that is in the guild's config.
The message will be prefixed with the Discord name of the person running the command.
If <server> has more than one word in it's name it will either need to be surrounded
by double quotes or the words separated by _"""
if await checks.is_rcon_admin(self.bot, ctx):
if server is not None:
server = server.replace('_', ' ').title()
if message is not None:
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name=server
)
if server_con:
msg = await ctx.send(f'Broadcasting "{message}" to {server}.')
response = await server_con.broadcast(message)
if response == 'Server received, But no response!!':
await msg.add_reaction(self.bot.unicode_emojis['y'])
else:
await msg.add_reaction(self.bot.unicode_emojis['x'])
else:
await ctx.send(f'{server} is not in the config for this guild')
else:
await ctx.send('You must include a message with this command.')
else:
await ctx.send('You must include a server with this command')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command(aliases=['servers', 'list_servers'])
@commands.guild_only()
@commands.check(checks.is_restricted_chan)
async def list_ark_servers(self, ctx):
"""Returns a list of all the ARK servers in the current guild\'s config."""
servers = ctx.guild_config.get('rcon_connections', [])
em = discord.Embed(style='rich',
title=f'__**There are currently {len(servers)} ARK servers in my config:**__',
color=discord.Colour.green()
)
if ctx.guild.icon:
em.set_thumbnail(url=f'{ctx.guild.icon_url}')
for server in servers:
description = f"""
**IP:** {servers[server]['ip']}:{servers[server]['port']}
**Steam Connect:** [steam://connect/{servers[server]['ip']}:{servers[server]['port']}]\
(steam://connect/{servers[server]['ip']}:{servers[server]['port']})"""
em.add_field(name=f'__***{server}***__', value=description, inline=False)
await ctx.send(embed=em)
@commands.command(name='server_chat')
@commands.guild_only()
async def send_chat_to_server(self, ctx, server: str=None, *, message: str=None):
if await checks.is_rcon_admin(self.bot, ctx):
if server is not None:
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(guild_config=ctx.guild_config,
name=server)
if server_con:
if message is not None:
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
msg = await ctx.send(f'Sending "{message}" to {server}\'s chat')
response = await server_con.serverchat(message)
if response == 'Server received, But no response!!':
await msg.add_reaction(self.bot.unicode_emojis['y'])
else:
await msg.add_reaction(self.bot.unicode_emojis['x'])
else:
await ctx.send('You must include a message with this command.')
else:
await ctx.send(f'That server is not in my configuration.\nPlease add it via !add_rcon_server '
f'"{server}" "ip" port "password" if you would like to get info from it.')
else:
await ctx.send('You must include a server with this command')
else:
await ctx.send('You are not authorized to run this command.')
@commands.command(name='restart_server', aliases=['restart'])
@commands.guild_only()
async def restart_rcon_server(self, ctx, message: str, server_name: str=None,
delay_time: int=15, sleep_time: int=60):
if await checks.is_rcon_admin(self.bot, ctx):
if server_name is None or server_name == 'all':
futures = []
rcon_servers: dict = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name='*'
)
for server_name, server_con in rcon_servers.items():
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
futures.append(utils.restart_rcon_server(ctx, server_name=server_name, server_con=server_con,
message=message, delay=delay_time, sleep=sleep_time))
self.bot.loop.create_task(asyncio.gather(*futures))
elif server_name.startswith('exclude='):
futures = []
exclude_servers = server_name.split('exclude=')[1].replace('_', ' ').title().split(',')
rcon_servers: dict = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name='*'
)
for server_name, server_con in rcon_servers.items():
if server_name not in exclude_servers:
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
futures.append(utils.restart_rcon_server(ctx, server_name=server_name, server_con=server_con,
message=message, delay=delay_time, sleep=sleep_time))
self.bot.loop.create_task(asyncio.gather(*futures))
else:
async with ctx.typing():
server_name = server_name.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name=server_name
)
if server_con:
message = ''.join(i for i in f'{ctx.author.display_name}: {message}' if ord(i) < 128)
await utils.restart_rcon_server(ctx, server_name=server_name, server_con=server_con,
message=message, delay=delay_time, sleep=sleep_time)
else:
await ctx.send(f'That server is not in my configuration.\nPlease add it via !add_rcon_server '
f'"{server_name}" "ip" port "password" if you would like to get info from it.')
else:
await ctx.send('You are not authorized to run this command.')
# noinspection PyShadowingNames
@staticmethod
async def start_dinowipe(channel, server_name: str, server_con: arcon.ARKServer):
msg = await channel.send(f'Wild dinos will be wiped on {server_name} in 2 minutes.')
await server_con.broadcast(f'Wild dino wipe incoming in 2 minutes, expect a small amount of lag while '
f'the dinos repopulate.')
await asyncio.sleep(90)
await server_con.serverchat(f'Wild Dino Wipe in 30 seconds')
await asyncio.sleep(26)
await server_con.serverchat(f'Wild Dino Wipe in 3')
await asyncio.sleep(1)
await server_con.serverchat(f'Wild Dino Wipe in 2')
await asyncio.sleep(1)
await server_con.serverchat(f'Wild Dino Wipe in 1')
await asyncio.sleep(1)
response = await server_con.destroy_wild_dinos()
if response == 'All Wild Dinos Destroyed':
await msg.edit(content=f'Wild Dinos Wiped on {server_name}')
await server_con.serverchat(f'Wild dinos have been wiped.')
else:
await msg.edit(content=f'Failed to wipe wild dinos on {server_name}')
await server_con.serverchat(f'Wild Dino Wipe failed, Please let the Admin know of this issue')
# noinspection PyShadowingNames,PyUnresolvedReferences
async def request_dinowipe(self, requester, server_name: str, server_con: arcon.ARKServer):
self.bot.dino_wipe_request[server_name] = True
if server_con.server_messages_channel and self.bot.get_channel(int(server_con.server_messages_channel)):
channel = self.bot.get_channel(int(server_con.server_messages_channel))
else:
channel = self.bot.get_channel(server_con.server_chat_channel)
msg = False
if channel:
msg = await channel.send(f'@here {requester.mention} has requested wild dinos be wiped on the '
f'{server_name} server\n'
f'If you are in the middle of taming or have some other reason to '
f'delay the wipe please react to this message with '
f'{self.bot.unicode_emojis["x"]}.\n'
f'Doing so will delay the wipe for 10 minutes.')
await server_con.broadcast(f'{requester.display_name} has requested wild dinos be wiped.\n'
f'if you are in the middle of taming or have some other reason to delay the wipe '
f'please send a chat message containing "$delaywipe" here in-game.\n'
f'Doing so will delay the wipe for 10 minutes.')
await asyncio.sleep(5)
await server_con.serverchat('The wipe will continue in 2 minutes if there are no requests to delay. '
'Send a message containing $delaywipe to delay for 10 minutes.')
rcon_log.info('Dino Wipe messages sent')
if msg:
def check(reaction, user):
return str(reaction.emoji) == self.bot.unicode_emojis['x'] and reaction.message.id == msg.id and \
user != self.bot.user
user = None
await msg.add_reaction(self.bot.unicode_emojis['x'])
try:
reaction, user = await self.bot.wait_for('reaction_add', check=check, timeout=120)
except asyncio.TimeoutError:
rcon_log.info('Message timed out...')
if self.bot.dino_wipe_request.get(server_name):
rcon_log.info('Starting dino wipe')
await self.start_dinowipe(channel, server_name, server_con)
rcon_log.info('Wild Dino Wipe Completed')
return
else:
rcon_log.info('Dino Wipe Delayed.')
await server_con.broadcast(f'Wild Dino Wipe has been delayed by {user.display_name}. '
f'Please watch chat for timing updates')
await channel.send(f'Dino wipe has been delayed by {user.mention if user else "Game Chat"}...')
for i in range(10):
await asyncio.sleep(60)
if i >= 5 or i == 0:
await server_con.serverchat(f'The Wild Dino Wipe process will continue in {9 - i} '
f'{"minute" if 9 - i == 1 else "minutes"}')
await self.request_dinowipe(requester, server_name, server_con)
else:
rcon_log.info('No channels configured, running wipe.')
await asyncio.sleep(120)
if not self.bot.dino_wipe_request.get(server_name):
rcon_log.info('Wipe Delay requested')
for i in range(10):
await asyncio.sleep(60)
await server_con.serverchat(f'The Wino Dino Wipe process will continue in {9 - i} '
f'{"minute" if 9 - i == 1 else "minutes"}')
await self.request_dinowipe(requester, server_name, server_con)
@commands.command(name='dinowipe')
@commands.guild_only()
async def run_dino_wipe(self, ctx, *, server=None):
"""Runs DestroyWildDinos on the specified ARK server.
If a server is not specified it will default to wiping the wild dinos on all servers in the guild\'s config.
Will print out "Wild Dinos Wiped on <server name>" for each server when the command completes successfully."""
if await checks.is_rcon_admin(self.bot, ctx):
futures = []
async with ctx.typing():
if server is None:
rcon_connections: dict = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name='*'
)
if rcon_connections:
for server_name, server_con in rcon_connections.items():
futures.append(self.start_dinowipe(ctx.channel, server_name, server_con))
self.bot.loop.create_task(asyncio.gather(*futures))
else:
await ctx.send('There are no available servers for this guild.')
else:
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name=server
)
if server_con:
# noinspection PyTypeChecker
await self.start_dinowipe(ctx.channel, server, server_con)
else:
await ctx.send(f'{server} is not currently in the configuration for this guild.')
else:
await ctx.send(f'You are not authorized to run this command.')
@commands.command(name='run_command', aliases=['run'])
@commands.guild_only()
async def run_rcon_command(self, ctx, server=None, *, command=None):
if await checks.is_rcon_admin(self.bot, ctx):
if not server:
await ctx.send('You must include a server when running this command.')
return
async with ctx.typing():
server = server.replace('_', ' ').title()
server_con: arcon.ARKServer = await self.get_rcon_server_by_name(
guild_config=ctx.guild_config, name=server
)
if server_con:
if not command:
await ctx.send('You must include a command to run on the server.')
return
command = command.split(' ')
if ctx.guild_config.get('allowed_rcon_commands'):
pass
response = await server_con.run_command(command=' '.join(command), multi_packet=True)
if isinstance(response, str):
await ctx.send(response)
else:
body = response.body
pag = utils.Paginator(bot=self.bot)
pag.add(body)
book = utils.Book(pag, (None, ctx.channel, self.bot, ctx.message))
await book.create_book()
else:
await ctx.send('That server was not found in the config for this guild.')
def setup(bot):
bot.add_cog(Rcon(bot))

1
geeksbot/exts/tickets.py Normal file
View File

@ -0,0 +1 @@
import discord

118
geeksbot/imports/arcon.py Normal file
View File

@ -0,0 +1,118 @@
from . import rcon
import asyncio
from typing import Union
import logging
arcon_log = logging.getLogger('arcon_lib')
class ARKServer(rcon.RCONConnection):
def __init__(self, *args, monitor_chat: bool = False, server_chat_channel: int = None,
server_messages_channel: int = None, **kwargs):
self.monitor_chat = monitor_chat
self.server_chat_channel = server_chat_channel
self.server_messages_channel = server_messages_channel
super().__init__(*args, **kwargs)
async def run_command(self, command: str, multi_packet: bool = False, reconnect_counter: int = 0) \
-> Union[rcon.RCONPacket, str]:
arcon_log.debug(f'Command requested: {command}')
if self.authenticated:
packet = rcon.RCONPacket(next(self.packet_id), rcon.SERVERDATA_EXECCOMMAND, command)
with await self.lock:
try:
arcon_log.debug(f'Sending packet {packet.packet_id}')
await self.send_packet(packet)
arcon_log.debug(f'Packet Sent.')
except ConnectionResetError:
arcon_log.info(f'Connection to {self.host}:{self.port} lost, Reconnecting...')
self.lock.release()
await self._reconnect_and_resend(packet)
await self.lock.acquire()
finally:
arcon_log.debug(f'Waiting for response to packet {packet.packet_id}')
try:
response = await self.read(packet, multi_packet=multi_packet)
except asyncio.TimeoutError as e:
if reconnect_counter > 5:
return 'Reached max reconnects. Closing connection.'
arcon_log.warning(f'No response received: {e}\nAttempting to reconnect #{reconnect_counter}')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
response = await self.run_command(command=command, multi_packet=multi_packet,
reconnect_counter=reconnect_counter + 1)
arcon_log.debug(f'Response Received:\n{response.packet_type}:{response.packet_id}:{response.body}')
response.body = response.body.strip('\x00\x00').strip()
return response
else:
return 'Server is not Authenticated. Please let the Admin know of this issue.'
async def getchat(self) -> str:
response = await self.run_command(command='getchat', multi_packet=True)
return response.body if isinstance(response, rcon.RCONPacket) else response
async def saveworld(self) -> str:
response = await self.run_command(command='saveworld')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def serverchat(self, message: str) -> str:
response = await self.run_command(command=f'serverchat {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def broadcast(self, message: str) -> str:
response = await self.run_command(command=f'broadcast {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def listplayers(self) -> str:
response = await self.run_command(command=f'listplayers')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def whitelist(self, steam_id: str) -> str:
response = await self.run_command(command=f'AllowPlayerToJoinNoCheck {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def ban_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'BanPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def unban_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'UnbanPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def kick_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'KickPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def stop_server(self) -> int:
saved = await self.saveworld()
if saved == 'World Saved':
await self.serverchat(saved)
await asyncio.sleep(10)
response = await self.run_command(command='DoExit')
if response.body == 'Exiting...':
return 0
else:
return 2
else:
return 1
async def get_logs(self):
response = await self.run_command(command=f'GetGameLog', multi_packet=True)
return response.body if isinstance(response, rcon.RCONPacket) else response
async def server_chat_to_steam_id(self, steam_id: int, message: str) -> str:
response = await self.run_command(command=f'ServerChatTo {steam_id} {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def server_chat_to_player_name(self, player_name: str, message: str) -> str:
response = await self.run_command(command=f'ServerChatToPlayer "{player_name}" {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def set_time_of_day(self, hour: int, minute: int = 00, seconds: int = 00) -> str:
response = await self.run_command(command=f'SetTimeOfDay {hour}:{minute}:{seconds}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def destroy_wild_dinos(self):
response = await self.run_command(command='DestroyWildDinos')
return response.body if isinstance(response, rcon.RCONPacket) else response

View File

@ -0,0 +1,4 @@
import discord
def is_rcon_admin(bot, ctx):

183
geeksbot/imports/rcon.py Normal file
View File

@ -0,0 +1,183 @@
import asyncio
import logging
import itertools
import struct
# Packet types
SERVERDATA_AUTH = 3
SERVERDATA_AUTH_RESPONSE = 2
SERVERDATA_EXECCOMMAND = 2
SERVERDATA_RESPONSE_VALUE = 0
__all__ = ['RCONPacket', 'RCONConnection']
rcon_log = logging.getLogger('rcon_lib')
class RCONPacket:
def __init__(self, packet_id: int = 0, packet_type: int = -1, body: str = ''):
self.packet_id = packet_id
self.packet_type = packet_type
self.body = body
def __str__(self):
"""Return the body of the packet"""
return self.body
def size(self):
"""Return the size of the packet"""
return len(self.body) + 10
def pack(self):
"""Return the packed packet"""
return struct.pack(f'<3i{len(self.body) + 2}s',
self.size(),
self.packet_id,
self.packet_type,
bytearray(self.body, 'utf-8'))
class RCONConnection:
"""Connection to an RCON server"""
def __init__(self, host: str, port: int, password: str = '', single_packet: bool = False):
"""Create a New RCON Connection
Parameters:
host (str): The hostname or IP address of the server to connect to
port (int): The port to connect to on the server
password (str): The password to authenticate with the server
single_packet (bool): True for servers who don't give 0 length SERVERDATA_RESPONSE_VALUE requests
"""
self.host = host
self.port = port
self.password = password
self.single_packet = single_packet
self.packet_id = itertools.count(1)
self.loop = asyncio.get_event_loop()
self.reader = None
self.writer = None
self.lock = asyncio.Lock()
self.authenticated = False
async def connect(self):
"""Returns -1 if connection times out
Returns 1 if connection and auth are successful
Returns 0 if auth fails"""
try:
rcon_log.debug(f'Connecting to {self.host}:{self.port}...')
self.reader, self.writer = await asyncio.open_connection(self.host, self.port, loop=self.loop)
except TimeoutError as e:
rcon_log.error(f'Timeout error: {e}')
return -1
else:
rcon_log.debug('Connected. Attempting to Authenticate...')
auth_packet = RCONPacket(next(self.packet_id), SERVERDATA_AUTH, self.password)
with await self.lock:
await self.send_packet(auth_packet)
response = await self.read()
if response.packet_type == SERVERDATA_AUTH_RESPONSE and response.packet_id != -1:
rcon_log.debug(f'Authorized {response.packet_type}:{response.packet_id}:{response.body}')
self.authenticated = True
return 1
else:
rcon_log.debug(f'Not Authorized {response.packet_type}:{response.packet_id}:{response.body}')
self.authenticated = False
return 0
async def _reconnect(self):
self.writer = None
self.reader = None
connected = await self.connect()
rcon_log.info(f'Connection completed with a return of {connected}')
if connected != -1:
rcon_log.info('Connected')
else:
rcon_log.warning('Connection Failed')
return connected
async def _reconnect_and_resend(self, packet):
connected = await self._reconnect()
if connected != -1:
await asyncio.sleep(0.1)
rcon_log.info(f'Re-sending packet {packet.packet_id}')
await self.send_packet(packet)
rcon_log.info(f'Packet Sent.')
return connected
else:
return connected
async def keep_alive(self):
while True:
await asyncio.sleep(60)
ka_packet = RCONPacket(next(self.packet_id), SERVERDATA_EXECCOMMAND, '')
try:
with await self.lock:
await asyncio.wait_for(self.send_packet(ka_packet), 10, loop=self.loop)
await asyncio.wait_for(self.read(ka_packet), 10, loop=self.loop)
except asyncio.TimeoutError:
self.reader = None
self.writer = None
await self.connect()
async def send_packet(self, packet):
if packet.size() > 4096:
rcon_log.error('Packet Size is larger than 4096 bytes. Cannot send packet.')
raise RuntimeWarning('Packet Size is larger than 4096 bytes. Cannot send packet.')
if self.writer is None:
await self.connect()
rcon_log.debug(f'Sending Packet {packet.packet_id}: {packet.pack() if packet.packet_type is not SERVERDATA_AUTH else "Censored for Password Security."}')
self.writer.write(packet.pack())
await self.writer.drain()
rcon_log.debug(f'Packet {packet.packet_id} Sent.')
async def read(self, request: RCONPacket = None, multi_packet=False) -> RCONPacket:
rcon_log.debug(f'Waiting to receive response to packet {request.packet_id if request else None}')
response = RCONPacket()
try:
if request:
while response.packet_id != request.packet_id and response.packet_id < request.packet_id:
if multi_packet:
if request is None:
rcon_log.warning('A request packet is required to receive a multi packet response')
raise ValueError('A request packet is required to receive a multi packet response')
await asyncio.sleep(.01)
response = await self._receive_multi_packet()
rcon_log.debug(f'Received Multi-Packet response to packet {request.packet_id}:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
else:
response = await self.receive_packet()
rcon_log.debug(f'Received Single-Packet response to packet {request.packet_id}:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
else:
response = await self.receive_packet()
rcon_log.debug(f'Received Single-Packet response:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
except struct.error as e:
rcon_log.error(f'Struct Error: {e}')
response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. '
'Please try again in a little bit.')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
except AttributeError as e:
rcon_log.error(f'Attribute Error: {e}')
response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. '
'Please try again in a little bit.')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
return response
async def receive_packet(self):
header = await self.reader.read(struct.calcsize('<3i'))
(packet_size, packet_id, packet_type) = struct.unpack('<3i', header)
body = await self.reader.read(packet_size - 8)
return RCONPacket(packet_id, packet_type, body.decode('ascii'))
async def _receive_multi_packet(self):
header = await self.reader.read(struct.calcsize('<3i'))
(packet_size, packet_id, packet_type) = struct.unpack('<3i', header)
body = await self.reader.readuntil(separator=b'\x00\x00')
return RCONPacket(packet_id, packet_type, body.decode('ascii'))

View File

@ -3,6 +3,10 @@ import asyncio
import typing import typing
async def get_guild_config(bot, guild_id):
guild_config = bot.cache.get()
# noinspection PyDefaultArgument # noinspection PyDefaultArgument
def to_list_of_str(items, out: list = list(), level=1, recurse=0): def to_list_of_str(items, out: list = list(), level=1, recurse=0):
# noinspection PyShadowingNames # noinspection PyShadowingNames

View File

@ -0,0 +1,9 @@
from django.urls import path
from .views import ChannelsAPI, ChannelDetail
app_name = "channels_api"
urlpatterns = [
path("", view=ChannelsAPI.as_view(), name="list"),
path("<str:id>/", view=ChannelDetail.as_view(), name='detail')
]

View File

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ChannelsConfig(AppConfig): class ChannelsConfig(AppConfig):
name = 'channels' name = 'geeksbot_v2.channels'
verbose_name = _("Channels")

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('guilds', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Channel',
fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-09-21 02:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('channels', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='channel',
name='admin',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='channel',
name='default',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='channel',
name='new_patron',
field=models.BooleanField(default=False),
),
]

View File

@ -12,6 +12,28 @@ from .utils import create_success_response
class Channel(models.Model): class Channel(models.Model):
id = models.CharField(max_length=30, primary_key=True) id = models.CharField(max_length=30, primary_key=True)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE) guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
default = models.BooleanField(default=False)
new_patron = models.BooleanField(default=False)
admin = models.BooleanField(default=False)
def update_channel(self, data):
if data.get('default'):
try:
existing_default = self.get_guild_channels(self.guild).get(default=True)
except ObjectDoesNotExist:
pass
else:
existing_default.default = False
existing_default.save()
finally:
self.default = data.get('default')
if data.get('new_patron'):
self.new_patron = data.get('new_patron')
if data.get('admin'):
self.admin = data.get('admin')
self.save()
return self
@classmethod @classmethod
def add_new_channel(cls, data): def add_new_channel(cls, data):
@ -21,7 +43,7 @@ class Channel(models.Model):
status=status.HTTP_409_CONFLICT) status=status.HTTP_409_CONFLICT)
guild_id = data.get('guild') guild_id = data.get('guild')
if not (id and guild_id): if not (id and guild_id):
return create_error_response('Id and Guild are required', return create_error_response('ID and Guild are required',
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
guild = Guild.get_guild_by_id(guild_id) guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild): if not isinstance(guild, Guild):
@ -30,7 +52,10 @@ class Channel(models.Model):
channel = cls( channel = cls(
id=id, id=id,
guild=guild guild=guild,
default=data.get('default', False),
new_patron=data.get('new_patron', False),
admin=data.get('admin', False)
) )
channel.save() channel.save()
return create_success_response(channel, status.HTTP_201_CREATED, many=False) return create_success_response(channel, status.HTTP_201_CREATED, many=False)
@ -42,5 +67,10 @@ class Channel(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@classmethod
def get_guild_channels(cls, guild):
if isinstance(guild, Guild):
return cls.objects.filter(guild=guild)
def __str__(self): def __str__(self):
return str(id) return str(id)

View File

@ -1,3 +1,69 @@
from django.shortcuts import render from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import ObjectDoesNotExist
from geeksbot_v2.utils.api_utils import PaginatedAPIView
from .models import Channel
from .utils import create_error_response
from .utils import create_success_response
# Create your views here. # Create your views here.
# API Views
class ChannelsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
guilds = Channel.objects.all()
page = self.paginate_queryset(guilds)
if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(guilds, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return Channel.add_new_channel(data)
class ChannelDetail(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id, format=None):
try:
guild = Channel.objects.get(id=id)
except ObjectDoesNotExist:
return create_error_response("Channel Does not Exist",
status=status.HTTP_404_NOT_FOUND)
else:
return create_success_response(guild,
status=status.HTTP_200_OK)
def put(self, request, id, format=None):
channel = Channel.get_channel_by_id(id)
if channel:
data = dict(request.data)
channel = channel.update_channel(data)
return create_success_response(channel,
status=status.HTTP_202_ACCEPTED)
else:
return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
def delete(self, request, id, format=None):
guild = Channel.get_guild_by_id(id)
if guild:
# data = dict(request.data)
# TODO Add a check to verify user is allowed to delete...
# Possibly in object permissions...
guild.delete()
return create_success_response(guild,
status=status.HTTP_200_OK)
else:
return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND)

View File

@ -80,6 +80,7 @@ LOCAL_APPS = [
"geeksbot_v2.dmessages.apps.MessagesConfig", "geeksbot_v2.dmessages.apps.MessagesConfig",
"geeksbot_v2.patreon.apps.PatreonConfig", "geeksbot_v2.patreon.apps.PatreonConfig",
"geeksbot_v2.rcon.apps.RconConfig", "geeksbot_v2.rcon.apps.RconConfig",
"geeksbot_v2.channels.apps.ChannelsConfig",
# Your stuff: custom apps go here # Your stuff: custom apps go here
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -285,3 +286,5 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 100, "PAGE_SIZE": 100,
} }
SILENCED_SYSTEM_CHECKS = ["auth.W004"]

View File

@ -16,8 +16,10 @@ urlpatterns = [
path("users/", include("geeksbot_v2.users.urls", namespace="users")), path("users/", include("geeksbot_v2.users.urls", namespace="users")),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here # Your stuff: custom urls includes go here
path("api/users", include("geeksbot_v2.users.api_urls", namespace="users_api")), path("api/users/", include("geeksbot_v2.users.api_urls", namespace="users_api")),
path("api/guilds", include("geeksbot_v2.guilds.api_urls", namespace="guilds_api")) path("api/guilds/", include("geeksbot_v2.guilds.api_urls", namespace="guilds_api")),
path("api/channels/", include("geeksbot_v2.channels.api_urls", namespace="channels_api")),
path("api/messages/", include("geeksbot_v2.dmessages.api_urls", namespace="messages_api")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:

View File

@ -0,0 +1,15 @@
from django.urls import path
from .views import MessageDetailAPI, MessagesAPI
from .views import RequestDetailAPI, RequestsAPI
from .views import CommentDetailAPI, CommentsAPI
app_name = "channels_api"
urlpatterns = [
path("", view=MessagesAPI.as_view(), name="message_list"),
path("<str:id>/", view=MessageDetailAPI.as_view(), name='message_detail'),
path("requests/", view=RequestsAPI.as_view(), name="requests_list"),
path("requests/<str:id>/", view=RequestDetailAPI.as_view(), name='request_detail'),
path("requests/<str:request_id>/comments/", view=CommentsAPI.as_view(), name="comments_list"),
path("requests/<str:request_id>/comments/<str:comment_id>/", view=CommentDetailAPI.as_view(), name='comment_detail'),
]

View File

@ -1,9 +1,7 @@
# Generated by Django 2.2.4 on 2019-09-17 19:31 # Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
import django.contrib.postgres.fields import django.contrib.postgres.fields
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -11,29 +9,26 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('guilds', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Message', name='AdminComment',
fields=[ fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('channel', models.CharField(max_length=30)), ('content', models.CharField(max_length=1000)),
('created_at', models.DateTimeField()), ('updated_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(null=True)), ],
('deleted_at', models.DateTimeField(null=True)), ),
migrations.CreateModel(
name='AdminRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('completed', models.BooleanField(default=False)),
('requested_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField(blank=True, default=None, null=True)),
('completed_message', models.CharField(blank=True, default=None, max_length=1000, null=True)),
('content', models.CharField(max_length=2000)), ('content', models.CharField(max_length=2000)),
('previous_content', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=2000), size=None)),
('tagged_users', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
('tagged_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
('tagged_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
('tagged_everyone', models.BooleanField()),
('embeds', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
('previous_embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None), size=None)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -45,20 +40,20 @@ class Migration(migrations.Migration):
('format', models.PositiveSmallIntegerField()), ('format', models.PositiveSmallIntegerField()),
('channel', models.CharField(max_length=30)), ('channel', models.CharField(max_length=30)),
('message_number', models.PositiveSmallIntegerField()), ('message_number', models.PositiveSmallIntegerField()),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
('message', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dmessages.Message')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='AdminRequest', name='Message',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('completed', models.BooleanField()), ('created_at', models.DateTimeField()),
('requested_at', models.DateTimeField()), ('modified_at', models.DateTimeField(blank=True, null=True)),
('completed_at', models.DateTimeField()), ('deleted_at', models.DateTimeField(blank=True, null=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('content', models.CharField(max_length=2000)),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')), ('previous_content', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=2000), default=list, size=None)),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dmessages.Message')), ('tagged_everyone', models.BooleanField()),
('embeds', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None)),
('previous_embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None), default=list, size=None)),
], ],
), ),
] ]

View File

@ -0,0 +1,95 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('guilds', '0001_initial'),
('dmessages', '0001_initial'),
('channels', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='message',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='message',
name='channel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='channels.Channel'),
),
migrations.AddField(
model_name='message',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='message',
name='tagged_channels',
field=models.ManyToManyField(related_name='_message_tagged_channels_+', to='channels.Channel'),
),
migrations.AddField(
model_name='message',
name='tagged_roles',
field=models.ManyToManyField(to='guilds.Role'),
),
migrations.AddField(
model_name='message',
name='tagged_users',
field=models.ManyToManyField(related_name='_message_tagged_users_+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='guildinfo',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='guildinfo',
name='message',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dmessages.Message'),
),
migrations.AddField(
model_name='adminrequest',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='adminrequest',
name='channel',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='channels.Channel'),
),
migrations.AddField(
model_name='adminrequest',
name='completed_by',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='adminrequest',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='adminrequest',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='dmessages.Message'),
),
migrations.AddField(
model_name='admincomment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='admincomment',
name='request',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dmessages.AdminRequest'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-21 07:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dmessages', '0002_auto_20190920_2139'),
]
operations = [
migrations.AlterField(
model_name='message',
name='content',
field=models.CharField(blank=True, max_length=2000, null=True),
),
]

View File

@ -19,20 +19,20 @@ from .utils import create_comment_success_response
class Message(models.Model): class Message(models.Model):
id = models.CharField(max_length=30, primary_key=True) id = models.CharField(max_length=30, primary_key=True)
author = models.ForeignKey(User, on_delete=models.CASCADE) author = models.ForeignKey(User, related_name="+", on_delete=models.CASCADE)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE) guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
channel = models.ForeignKey(Channel, on_delete=models.CASCADE) channel = models.ForeignKey(Channel, related_name="+", on_delete=models.CASCADE)
created_at = models.DateTimeField() created_at = models.DateTimeField()
modified_at = models.DateTimeField(null=True, blank=True) modified_at = models.DateTimeField(null=True, blank=True)
deleted_at = models.DateTimeField(null=True, blank=True) deleted_at = models.DateTimeField(null=True, blank=True)
content = models.CharField(max_length=2000) content = models.CharField(max_length=2000, null=True, blank=True)
previous_content = ArrayField(models.CharField(max_length=2000), default=[]) previous_content = ArrayField(models.CharField(max_length=2000), default=list)
tagged_users = models.ManyToManyField(User) tagged_users = models.ManyToManyField(User, related_name="+")
tagged_channels = models.ManyToManyField(Channel) tagged_channels = models.ManyToManyField(Channel, related_name="+")
tagged_roles = models.ManyToManyField(Role) tagged_roles = models.ManyToManyField(Role)
tagged_everyone = models.BooleanField() tagged_everyone = models.BooleanField()
embeds = ArrayField(models.TextField(), default=[]) embeds = ArrayField(models.TextField(), default=list)
previous_embeds = ArrayField(ArrayField(models.TextField()), default=[]) previous_embeds = ArrayField(ArrayField(models.TextField()), default=list)
@classmethod @classmethod
def add_new_message(cls, data): def add_new_message(cls, data):
@ -46,7 +46,7 @@ class Message(models.Model):
created_at = data.get('created_at') created_at = data.get('created_at')
content = data.get('content') content = data.get('content')
tagged_everyone = data.get('tagged_everyone') tagged_everyone = data.get('tagged_everyone')
if not (id and author_id and guild_id and channel_id and created_at and content and tagged_everyone): if not (id and author_id and guild_id and channel_id and created_at and (tagged_everyone is not None)):
return create_error_response("One or more required fields are missing.", return create_error_response("One or more required fields are missing.",
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
author = User.get_user_by_id(author_id) author = User.get_user_by_id(author_id)
@ -97,9 +97,9 @@ class Message(models.Model):
def update_message(self, data): def update_message(self, data):
if data.get('modified_at'): if data.get('modified_at'):
self.modified_at = data.get('modified_at') self.modified_at = datetime.fromtimestamp(int(data.get('modified_at')))
if data.get('deleted_at'): if data.get('deleted_at'):
self.modified_at = data.get('deleted_at') self.deleted_at = datetime.fromtimestamp(int(data.get('deleted_at')))
if data.get('content'): if data.get('content'):
content = data.get('content') content = data.get('content')
if content != self.content: if content != self.content:
@ -164,13 +164,15 @@ class GuildInfo(models.Model):
class AdminRequest(models.Model): class AdminRequest(models.Model):
guild = models.ForeignKey(Guild, on_delete=models.CASCADE) guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.DO_NOTHING) author = models.ForeignKey(User, related_name="+", on_delete=models.DO_NOTHING)
message = models.ForeignKey(Message, on_delete=models.DO_NOTHING) message = models.ForeignKey(Message, on_delete=models.DO_NOTHING)
channel = models.ForeignKey(Channel, on_delete=models.DO_NOTHING) channel = models.ForeignKey(Channel, on_delete=models.DO_NOTHING, null=True)
completed = models.BooleanField(default=False) completed = models.BooleanField(default=False)
requested_at = models.DateTimeField(auto_now_add=True, blank=True) requested_at = models.DateTimeField(auto_now_add=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True, default=None) completed_at = models.DateTimeField(null=True, blank=True, default=None)
completed_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True, default=None) completed_by = models.ForeignKey(
User, related_name="+", on_delete=models.DO_NOTHING, null=True, blank=True, default=None
)
completed_message = models.CharField(max_length=1000, null=True, blank=True, default=None) completed_message = models.CharField(max_length=1000, null=True, blank=True, default=None)
content = models.CharField(max_length=2000) content = models.CharField(max_length=2000)
@ -179,7 +181,7 @@ class AdminRequest(models.Model):
completed_by_id = data.get('completed_by') completed_by_id = data.get('completed_by')
completed_message = data.get('message') completed_message = data.get('message')
if not self.completed and completed: if not self.completed and completed:
self.completed_at = datetime.now() self.completed_at = datetime.utcnow()
self.completed_message = completed_message self.completed_message = completed_message
user = User.get_user_by_id(completed_by_id) user = User.get_user_by_id(completed_by_id)
if not isinstance(user, User): if not isinstance(user, User):
@ -226,6 +228,10 @@ class AdminRequest(models.Model):
request.save() request.save()
return create_request_success_response(request, status.HTTP_201_CREATED, many=False) return create_request_success_response(request, status.HTTP_201_CREATED, many=False)
@classmethod
def get_open_requests_by_guild(cls, guild_id):
return cls.objects.filter(guild__id=guild_id).filter(completed=False)
@classmethod @classmethod
def get_request_by_id(cls, id): def get_request_by_id(cls, id):
try: try:
@ -244,8 +250,7 @@ class AdminComment(models.Model):
updated_at = models.DateTimeField(auto_now_add=True, blank=True) updated_at = models.DateTimeField(auto_now_add=True, blank=True)
@classmethod @classmethod
def add_new_comment(cls, data): def add_new_comment(cls, data, request_id):
request_id = data.get('request')
author_id = data.get('author') author_id = data.get('author')
content = data.get('content') content = data.get('content')
if not (request_id and author_id and content): if not (request_id and author_id and content):
@ -274,3 +279,7 @@ class AdminComment(models.Model):
return cls.objects.get(id=id) return cls.objects.get(id=id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@classmethod
def get_comments_by_request(cls, request):
return cls.objects.filter(request=request)

View File

@ -1,3 +1,124 @@
from django.shortcuts import render from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.response import Response
from rest_framework import status
from .models import Message
from .models import AdminComment
from .models import AdminRequest
from .models import GuildInfo
from geeksbot_v2.utils.api_utils import PaginatedAPIView
from .utils import create_error_response
from .utils import create_success_response
from .utils import create_request_success_response
from .utils import create_comment_success_response
from .serializers import AdminRequestSerializer
from .serializers import AdminCommentSerializer
# Create your views here. # Create your views here.
# API Views
class MessagesAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
messages = Message.objects.all()
page = self.paginate_queryset(messages)
if page:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(messages, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return Message.add_new_message(data)
class MessageDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id, format=None):
message = Message.get_message_by_id(id)
if message:
return create_success_response(message, status.HTTP_200_OK, many=False)
else:
return create_error_response("Message Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def put(self, request, id, format=None):
data = dict(request.data)
message = Message.get_message_by_id(id)
if message:
return message.update_message(data)
else:
return create_error_response('Message Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
class RequestsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild, format=None):
requests = AdminRequest.get_open_requests_by_guild(guild)
page = self.paginate_queryset(requests)
if page is not None:
return create_request_success_response(page, status.HTTP_200_OK, many=True)
return create_request_success_response(requests, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return AdminRequest.add_new_request(data)
class RequestDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, req, id, format=None):
req = AdminRequest.get_request_by_id(id)
if req:
comments = AdminComment.get_comments_by_request(req)
if comments:
data = AdminRequestSerializer(req).data
data['comments'] = AdminCommentSerializer(comments, many=True).data
return Response(data, status.HTTP_200_OK)
else:
return create_request_success_response(req, status.HTTP_200_OK, many=False)
else:
return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def put(self, request, id, format=None):
req = AdminRequest.get_request_by_id(id)
if req:
data = dict(request.data)
return req.update_request(data)
return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
class CommentsAPI(PaginatedAPIView):
permissions_classes = [IsAuthenticated]
def post(self, request, request_id, format=None):
data = dict(request.data)
return AdminComment.add_new_comment(data, request_id)
class CommentDetailAPI(APIView):
permissions_classes = [IsAuthenticated]
def get(self, request, request_id, comment_id, format=None):
comment = AdminComment.get_comment_by_id(comment_id)
if comment:
if comment.request.id != request_id:
return create_error_response("That comment is not for this request",
status=status.HTTP_400_BAD_REQUEST)
return create_comment_success_response(comment, status.HTTP_200_OK, many=False)
else:
return create_error_response("Comment Does Not Exist",
status=status.HTTP_404_NOT_FOUND)

View File

@ -36,7 +36,7 @@ done
>&2 echo 'PostgreSQL is available' >&2 echo 'PostgreSQL is available'
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
python manage.py makemigrations python manage.py makemigrations --noinput
python manage.py migrate python manage.py migrate
/usr/bin/supervisord -c /etc/supervisor/supervisord.conf /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

View File

@ -1,9 +1,12 @@
from django.urls import path from django.urls import path
from .views import GuildsAPI, GuildDetail from .views import GuildsAPI, GuildDetail
from .views import RolesAPI, RoleDetailAPI
app_name = "users_api" app_name = "guilds_api"
urlpatterns = [ urlpatterns = [
path("/", view=GuildsAPI.as_view(), name="list"), path("", view=GuildsAPI.as_view(), name="list"),
path("/<str:id>/", view=GuildDetail.as_view(), name='detail') path("<str:id>/", view=GuildDetail.as_view(), name='detail'),
path("<str:guild_id>/roles/", view=RolesAPI.as_view(), name="list"),
path("<str:guild_id>/roles/<str:id>/", view=RoleDetailAPI.as_view(), name='detail'),
] ]

View File

@ -1,4 +1,4 @@
# Generated by Django 2.2.4 on 2019-09-17 19:31 # Generated by Django 2.2.4 on 2019-09-20 21:39
import django.contrib.postgres.fields import django.contrib.postgres.fields
from django.db import migrations, models from django.db import migrations, models
@ -28,7 +28,7 @@ class Migration(migrations.Migration):
name='Role', name='Role',
fields=[ fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)), ('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('type', models.PositiveSmallIntegerField()), ('role_type', models.PositiveSmallIntegerField()),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')), ('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
], ],
), ),

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.4 on 2019-09-21 02:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('guilds', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='guild',
name='admin_chat',
),
migrations.RemoveField(
model_name='guild',
name='default_channel',
),
migrations.RemoveField(
model_name='guild',
name='new_patron_channel',
),
]

View File

@ -15,24 +15,15 @@ from .utils import create_role_success_response
class Guild(models.Model): class Guild(models.Model):
id = models.CharField(max_length=30, primary_key=True) id = models.CharField(max_length=30, primary_key=True)
admin_chat = models.CharField(max_length=30, blank=True, null=True)
new_patron_message = models.TextField(max_length=1000, blank=True, null=True) new_patron_message = models.TextField(max_length=1000, blank=True, null=True)
default_channel = models.CharField(max_length=30)
new_patron_channel = models.CharField(max_length=30, blank=True, null=True)
prefixes = ArrayField(models.CharField(max_length=10)) prefixes = ArrayField(models.CharField(max_length=10))
def __str__(self): def __str__(self):
return self.id return self.id
def update_guild(self, data): def update_guild(self, data):
if data.get('admin_chat'):
self.admin_chat = data.get('admin_chat')
if data.get('new_patron_message'): if data.get('new_patron_message'):
self.new_patron_message = data.get('new_patron_message') self.new_patron_message = data.get('new_patron_message')
if data.get('default_channel'):
self.default_channel = data.get('default_channel')
if data.get('new_patron_channel'):
self.new_patron_channel = data.get('new_patron_channel')
if data.get('add_prefix'): if data.get('add_prefix'):
if data.get('add_prefix') not in self.prefixes: if data.get('add_prefix') not in self.prefixes:
self.prefixes.append(data.get('add_prefix')) self.prefixes.append(data.get('add_prefix'))
@ -55,9 +46,8 @@ class Guild(models.Model):
@classmethod @classmethod
def create_guild(cls, data): def create_guild(cls, data):
id = data.get('id') id = data.get('id')
default_channel = data.get('default_channel') if not id:
if not (id and default_channel): return create_error_response('ID is required',
return create_error_response('id and default_channel are required',
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
if cls.get_guild_by_id(id): if cls.get_guild_by_id(id):
@ -66,11 +56,8 @@ class Guild(models.Model):
guild = cls( guild = cls(
id=id, id=id,
default_channel=default_channel,
prefixes=data.get('prefixes'), prefixes=data.get('prefixes'),
admin_chat=data.get('admin_chat'), new_patron_message=data.get('new_patron_message')
new_patron_message=data.get('new_patron_message'),
new_patron_channel=data.get('new_patron_channel')
) )
guild.save() guild.save()
return create_success_response(guild, status.HTTP_201_CREATED, many=False) return create_success_response(guild, status.HTTP_201_CREATED, many=False)
@ -89,11 +76,10 @@ class Role(models.Model):
return create_role_success_response(self, status=status.HTTP_202_ACCEPTED, many=False) return create_role_success_response(self, status=status.HTTP_202_ACCEPTED, many=False)
@classmethod @classmethod
def add_new_role(cls, data): def add_new_role(cls, guild_id, data):
id = data.get('id') id = data.get('id')
guild_id = data.get('guild')
role_type = data.get('role_type') role_type = data.get('role_type')
if not (id and guild_id and role_type): if not (id and guild_id and (role_type is not None)):
return create_error_response("The Role ID, Guild, and Role Type are required", return create_error_response("The Role ID, Guild, and Role Type are required",
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
@ -132,5 +118,9 @@ class Role(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@classmethod
def get_guild_roles(cls, guild):
return cls.objects.filter(guild__id=guild)
def __str__(self): def __str__(self):
return f"{self.guild.id} | {self.id}" return f"{self.guild.id} | {self.id}"

View File

@ -13,4 +13,4 @@ class GuildSerializer(serializers.ModelSerializer):
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
fields = ["id", "guild", "type"] fields = ["id", "guild", "role_type"]

View File

@ -5,8 +5,10 @@ from django.core.exceptions import ObjectDoesNotExist
from geeksbot_v2.utils.api_utils import PaginatedAPIView from geeksbot_v2.utils.api_utils import PaginatedAPIView
from .models import Guild from .models import Guild
from .models import Role
from .utils import create_error_response from .utils import create_error_response
from .utils import create_success_response from .utils import create_success_response
from .utils import create_role_success_response
# Create your views here. # Create your views here.
@ -67,3 +69,45 @@ class GuildDetail(APIView):
else: else:
return create_error_response('Guild Does Not Exist', return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
class RolesAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
roles = Role.get_guild_roles(guild_id)
page = self.paginate_queryset(roles)
if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(roles, status.HTTP_200_OK, many=True)
def post(self, request, guild_id, format=None):
data = dict(request.data)
return Role.add_new_role(guild_id, data)
class RoleDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, id, format=None):
try:
role = Role.objects.get(id=id)
except ObjectDoesNotExist:
return create_error_response("Guild Does not Exist",
status=status.HTTP_404_NOT_FOUND)
else:
return create_role_success_response(role,
status=status.HTTP_200_OK)
def put(self, request, guild_id, id, format=None):
role = Role.get_role_by_id(id)
if role:
data = dict(request.data)
role = role.update_role(data)
return create_role_success_response(role,
status=status.HTTP_202_ACCEPTED)
else:
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)

View File

@ -1,4 +1,4 @@
# Generated by Django 2.2.4 on 2019-09-17 19:31 # Generated by Django 2.2.4 on 2019-09-20 21:39
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -16,10 +16,9 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='PatreonCreator', name='PatreonCreator',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('creator', models.CharField(max_length=50, primary_key=True, serialize=False)),
('creator', models.CharField(max_length=50)), ('link', models.CharField(max_length=100, unique=True)),
('link', models.CharField(max_length=100)), ('guilds', models.ManyToManyField(to='guilds.Guild')),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -30,7 +29,8 @@ class Migration(migrations.Migration):
('description', models.TextField()), ('description', models.TextField()),
('amount', models.IntegerField(null=True)), ('amount', models.IntegerField(null=True)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patreon.PatreonCreator')), ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patreon.PatreonCreator')),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')), ('guild', models.ManyToManyField(to='guilds.Guild')),
('next_lower_tier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='patreon.PatreonTier')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Role')), ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Role')),
], ],
), ),

View File

@ -70,7 +70,7 @@ class PatreonTier(models.Model):
description = models.TextField() description = models.TextField()
role = models.ForeignKey(Role, on_delete=models.CASCADE) role = models.ForeignKey(Role, on_delete=models.CASCADE)
amount = models.IntegerField(null=True) amount = models.IntegerField(null=True)
next_lower_tier = models.ForeignKey('self', null=True, blank=True) next_lower_tier = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True)
def update_tier(self, data): def update_tier(self, data):
if data.get('guild'): if data.get('guild'):

View File

@ -1,6 +1,5 @@
# Generated by Django 2.2.4 on 2019-09-17 19:31 # Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -10,9 +9,9 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('dmessages', '0001_initial'),
('guilds', '0001_initial'), ('guilds', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('dmessages', '0001_initial'),
('channels', '0001_initial'),
] ]
operations = [ operations = [
@ -25,13 +24,12 @@ class Migration(migrations.Migration):
('port', models.PositiveIntegerField()), ('port', models.PositiveIntegerField()),
('password', models.CharField(max_length=50)), ('password', models.CharField(max_length=50)),
('monitor_chat', models.BooleanField()), ('monitor_chat', models.BooleanField()),
('monitor_chat_channel', models.CharField(blank=True, max_length=30)), ('alerts_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('alerts_channel', models.CharField(blank=True, max_length=30)),
('info_channel', models.CharField(blank=True, max_length=30)),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')), ('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
('info_message', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dmessages.Message')), ('info_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('settings_message', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dmessages.Message')), ('info_message', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='dmessages.Message')),
('whitelist', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), ('monitor_chat_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('settings_message', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='dmessages.Message')),
], ],
), ),
] ]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('rcon', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='rconserver',
name='whitelist',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -4,6 +4,6 @@ from geeksbot_v2.users.views import UsersAPI, UserDetail
app_name = "users_api" app_name = "users_api"
urlpatterns = [ urlpatterns = [
path("/", view=UsersAPI.as_view(), name="list"), path("", view=UsersAPI.as_view(), name="list"),
path("/<str:id>/", view=UserDetail.as_view(), name="detail"), path("<str:id>/", view=UserDetail.as_view(), name="detail"),
] ]

View File

@ -1,4 +1,4 @@
# Generated by Django 2.2.4 on 2019-09-17 19:38 # Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
@ -25,7 +25,6 @@ class Migration(migrations.Migration):
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
@ -33,11 +32,12 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')), ('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')),
('username', models.CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('id', models.CharField(max_length=30, primary_key=True, serialize=False)), ('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('discord_username', models.CharField(max_length=100, null=True)), ('discord_username', models.CharField(max_length=100, null=True)),
('previous_discord_usernames', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)), ('previous_discord_usernames', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)),
('discriminator', models.IntegerField(null=True)), ('discriminator', models.CharField(max_length=4, null=True)),
('previous_discriminators', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, null=True, size=None)), ('previous_discriminators', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None)),
('steam_id', models.CharField(blank=True, max_length=30, null=True)), ('steam_id', models.CharField(blank=True, max_length=30, null=True)),
('animated', models.BooleanField(blank=True, null=True)), ('animated', models.BooleanField(blank=True, null=True)),
('avatar', models.CharField(blank=True, max_length=100, null=True)), ('avatar', models.CharField(blank=True, max_length=100, null=True)),
@ -61,9 +61,9 @@ class Migration(migrations.Migration):
name='UserLog', name='UserLog',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()), ('time', models.DateTimeField(auto_now_add=True)),
('action', models.IntegerField()), ('action', models.IntegerField()),
('description', models.CharField(max_length=100)), ('description', models.CharField(blank=True, max_length=100, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),

View File

@ -1,19 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-17 21:09
import django.contrib.auth.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-18 05:54
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20190917_2109'),
]
operations = [
migrations.AlterField(
model_name='user',
name='discriminator',
field=models.CharField(max_length=4, null=True),
),
migrations.AlterField(
model_name='user',
name='previous_discriminators',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None),
),
]

View File

@ -6,3 +6,4 @@ psutil
pytz pytz
async_timeout async_timeout
cached_property cached_property
redis-py