diff --git a/geeksbot/__main__.py b/geeksbot/__main__.py index 0f1c31d..da6a9e9 100644 --- a/geeksbot/__main__.py +++ b/geeksbot/__main__.py @@ -57,6 +57,8 @@ import json # noqa: E402 logger.info('JSON Imported') import aiohttp # noqa: E402 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') # noinspection PyRedeclaration # start = datetime.utcnow() @@ -78,8 +80,11 @@ class Geeksbot(commands.Bot): self.config_dir = 'geeksbot/config/' self.config_file = 'bot_config.json' 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.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: self.bot_config = json.load(f) self.embed_color = discord.Colour.from_rgb(49, 107, 111) diff --git a/geeksbot/config/bot_config.json b/geeksbot/config/bot_config.json index 0d28390..fe58878 100644 --- a/geeksbot/config/bot_config.json +++ b/geeksbot/config/bot_config.json @@ -1,5 +1,7 @@ { "load_list": [ - "admin" + "admin", + "exec", + "message_events" ] } \ No newline at end of file diff --git a/geeksbot/exts/message_events.py b/geeksbot/exts/message_events.py new file mode 100644 index 0000000..d0cb469 --- /dev/null +++ b/geeksbot/exts/message_events.py @@ -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)) diff --git a/geeksbot/exts/rcon.py b/geeksbot/exts/rcon.py new file mode 100644 index 0000000..07decb8 --- /dev/null +++ b/geeksbot/exts/rcon.py @@ -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 (, , ) 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 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 " 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)) diff --git a/geeksbot/exts/tickets.py b/geeksbot/exts/tickets.py new file mode 100644 index 0000000..e3ae20d --- /dev/null +++ b/geeksbot/exts/tickets.py @@ -0,0 +1 @@ +import discord diff --git a/geeksbot/imports/arcon.py b/geeksbot/imports/arcon.py new file mode 100644 index 0000000..77126b4 --- /dev/null +++ b/geeksbot/imports/arcon.py @@ -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 diff --git a/geeksbot/imports/checks.py b/geeksbot/imports/checks.py index e69de29..1543645 100644 --- a/geeksbot/imports/checks.py +++ b/geeksbot/imports/checks.py @@ -0,0 +1,4 @@ +import discord + +def is_rcon_admin(bot, ctx): + \ No newline at end of file diff --git a/geeksbot/imports/rcon.py b/geeksbot/imports/rcon.py new file mode 100644 index 0000000..1782f2e --- /dev/null +++ b/geeksbot/imports/rcon.py @@ -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')) diff --git a/geeksbot/imports/utils.py b/geeksbot/imports/utils.py index 531451d..cd204b1 100644 --- a/geeksbot/imports/utils.py +++ b/geeksbot/imports/utils.py @@ -3,6 +3,10 @@ import asyncio import typing +async def get_guild_config(bot, guild_id): + guild_config = bot.cache.get() + + # noinspection PyDefaultArgument def to_list_of_str(items, out: list = list(), level=1, recurse=0): # noinspection PyShadowingNames diff --git a/geeksbot_v2/channels/api_urls.py b/geeksbot_v2/channels/api_urls.py new file mode 100644 index 0000000..0b54f91 --- /dev/null +++ b/geeksbot_v2/channels/api_urls.py @@ -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("/", view=ChannelDetail.as_view(), name='detail') +] diff --git a/geeksbot_v2/channels/apps.py b/geeksbot_v2/channels/apps.py index b1b71ee..ebbced1 100644 --- a/geeksbot_v2/channels/apps.py +++ b/geeksbot_v2/channels/apps.py @@ -1,5 +1,7 @@ from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ class ChannelsConfig(AppConfig): - name = 'channels' + name = 'geeksbot_v2.channels' + verbose_name = _("Channels") diff --git a/geeksbot_v2/channels/migrations/0001_initial.py b/geeksbot_v2/channels/migrations/0001_initial.py new file mode 100644 index 0000000..e1a6387 --- /dev/null +++ b/geeksbot_v2/channels/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/geeksbot_v2/channels/migrations/0002_auto_20190921_0250.py b/geeksbot_v2/channels/migrations/0002_auto_20190921_0250.py new file mode 100644 index 0000000..b2cef5a --- /dev/null +++ b/geeksbot_v2/channels/migrations/0002_auto_20190921_0250.py @@ -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), + ), + ] diff --git a/geeksbot_v2/channels/models.py b/geeksbot_v2/channels/models.py index 225b703..6c216fc 100644 --- a/geeksbot_v2/channels/models.py +++ b/geeksbot_v2/channels/models.py @@ -12,6 +12,28 @@ from .utils import create_success_response class Channel(models.Model): id = models.CharField(max_length=30, primary_key=True) 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 def add_new_channel(cls, data): @@ -21,7 +43,7 @@ class Channel(models.Model): status=status.HTTP_409_CONFLICT) guild_id = data.get('guild') 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) guild = Guild.get_guild_by_id(guild_id) if not isinstance(guild, Guild): @@ -30,7 +52,10 @@ class Channel(models.Model): channel = cls( 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() return create_success_response(channel, status.HTTP_201_CREATED, many=False) @@ -41,6 +66,11 @@ class Channel(models.Model): return cls.objects.get(id=id) except ObjectDoesNotExist: return None + + @classmethod + def get_guild_channels(cls, guild): + if isinstance(guild, Guild): + return cls.objects.filter(guild=guild) def __str__(self): return str(id) diff --git a/geeksbot_v2/channels/views.py b/geeksbot_v2/channels/views.py index 91ea44a..2edf1f5 100644 --- a/geeksbot_v2/channels/views.py +++ b/geeksbot_v2/channels/views.py @@ -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. + +# 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) diff --git a/geeksbot_v2/config/settings/base.py b/geeksbot_v2/config/settings/base.py index 9e41752..460b496 100644 --- a/geeksbot_v2/config/settings/base.py +++ b/geeksbot_v2/config/settings/base.py @@ -80,6 +80,7 @@ LOCAL_APPS = [ "geeksbot_v2.dmessages.apps.MessagesConfig", "geeksbot_v2.patreon.apps.PatreonConfig", "geeksbot_v2.rcon.apps.RconConfig", + "geeksbot_v2.channels.apps.ChannelsConfig", # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -285,3 +286,5 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 100, } + +SILENCED_SYSTEM_CHECKS = ["auth.W004"] diff --git a/geeksbot_v2/config/urls.py b/geeksbot_v2/config/urls.py index 3920996..1eb6ea8 100644 --- a/geeksbot_v2/config/urls.py +++ b/geeksbot_v2/config/urls.py @@ -16,8 +16,10 @@ urlpatterns = [ path("users/", include("geeksbot_v2.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here - 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/users/", include("geeksbot_v2.users.api_urls", namespace="users_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) if settings.DEBUG: diff --git a/geeksbot_v2/dmessages/api_urls.py b/geeksbot_v2/dmessages/api_urls.py new file mode 100644 index 0000000..db96aef --- /dev/null +++ b/geeksbot_v2/dmessages/api_urls.py @@ -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("/", view=MessageDetailAPI.as_view(), name='message_detail'), + path("requests/", view=RequestsAPI.as_view(), name="requests_list"), + path("requests//", view=RequestDetailAPI.as_view(), name='request_detail'), + path("requests//comments/", view=CommentsAPI.as_view(), name="comments_list"), + path("requests//comments//", view=CommentDetailAPI.as_view(), name='comment_detail'), +] diff --git a/geeksbot_v2/dmessages/migrations/0001_initial.py b/geeksbot_v2/dmessages/migrations/0001_initial.py index 1927f12..bdb189d 100644 --- a/geeksbot_v2/dmessages/migrations/0001_initial.py +++ b/geeksbot_v2/dmessages/migrations/0001_initial.py @@ -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 from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -11,29 +9,26 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('guilds', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Message', + name='AdminComment', fields=[ - ('id', models.CharField(max_length=30, primary_key=True, serialize=False)), - ('channel', models.CharField(max_length=30)), - ('created_at', models.DateTimeField()), - ('modified_at', models.DateTimeField(null=True)), - ('deleted_at', models.DateTimeField(null=True)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.CharField(max_length=1000)), + ('updated_at', models.DateTimeField(auto_now_add=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)), - ('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( @@ -45,20 +40,20 @@ class Migration(migrations.Migration): ('format', models.PositiveSmallIntegerField()), ('channel', models.CharField(max_length=30)), ('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( - name='AdminRequest', + name='Message', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('completed', models.BooleanField()), - ('requested_at', models.DateTimeField()), - ('completed_at', models.DateTimeField()), - ('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')), - ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dmessages.Message')), + ('id', models.CharField(max_length=30, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField()), + ('modified_at', models.DateTimeField(blank=True, null=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('content', models.CharField(max_length=2000)), + ('previous_content', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=2000), default=list, size=None)), + ('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)), ], ), ] diff --git a/geeksbot_v2/dmessages/migrations/0002_auto_20190920_2139.py b/geeksbot_v2/dmessages/migrations/0002_auto_20190920_2139.py new file mode 100644 index 0000000..4258381 --- /dev/null +++ b/geeksbot_v2/dmessages/migrations/0002_auto_20190920_2139.py @@ -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'), + ), + ] diff --git a/geeksbot_v2/dmessages/migrations/0003_auto_20190921_0721.py b/geeksbot_v2/dmessages/migrations/0003_auto_20190921_0721.py new file mode 100644 index 0000000..5f3b8f3 --- /dev/null +++ b/geeksbot_v2/dmessages/migrations/0003_auto_20190921_0721.py @@ -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), + ), + ] diff --git a/geeksbot_v2/dmessages/models.py b/geeksbot_v2/dmessages/models.py index 8d49192..4f9d863 100644 --- a/geeksbot_v2/dmessages/models.py +++ b/geeksbot_v2/dmessages/models.py @@ -19,20 +19,20 @@ from .utils import create_comment_success_response class Message(models.Model): 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) - channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + channel = models.ForeignKey(Channel, related_name="+", on_delete=models.CASCADE) created_at = models.DateTimeField() modified_at = models.DateTimeField(null=True, blank=True) deleted_at = models.DateTimeField(null=True, blank=True) - content = models.CharField(max_length=2000) - previous_content = ArrayField(models.CharField(max_length=2000), default=[]) - tagged_users = models.ManyToManyField(User) - tagged_channels = models.ManyToManyField(Channel) + content = models.CharField(max_length=2000, null=True, blank=True) + previous_content = ArrayField(models.CharField(max_length=2000), default=list) + tagged_users = models.ManyToManyField(User, related_name="+") + tagged_channels = models.ManyToManyField(Channel, related_name="+") tagged_roles = models.ManyToManyField(Role) tagged_everyone = models.BooleanField() - embeds = ArrayField(models.TextField(), default=[]) - previous_embeds = ArrayField(ArrayField(models.TextField()), default=[]) + embeds = ArrayField(models.TextField(), default=list) + previous_embeds = ArrayField(ArrayField(models.TextField()), default=list) @classmethod def add_new_message(cls, data): @@ -46,7 +46,7 @@ class Message(models.Model): created_at = data.get('created_at') content = data.get('content') 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.", status=status.HTTP_400_BAD_REQUEST) author = User.get_user_by_id(author_id) @@ -97,9 +97,9 @@ class Message(models.Model): def update_message(self, data): 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'): - self.modified_at = data.get('deleted_at') + self.deleted_at = datetime.fromtimestamp(int(data.get('deleted_at'))) if data.get('content'): content = data.get('content') if content != self.content: @@ -164,13 +164,15 @@ class GuildInfo(models.Model): class AdminRequest(models.Model): 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) - 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) requested_at = models.DateTimeField(auto_now_add=True, blank=True) 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) content = models.CharField(max_length=2000) @@ -179,7 +181,7 @@ class AdminRequest(models.Model): completed_by_id = data.get('completed_by') completed_message = data.get('message') if not self.completed and completed: - self.completed_at = datetime.now() + self.completed_at = datetime.utcnow() self.completed_message = completed_message user = User.get_user_by_id(completed_by_id) if not isinstance(user, User): @@ -226,6 +228,10 @@ class AdminRequest(models.Model): request.save() 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 def get_request_by_id(cls, id): try: @@ -244,8 +250,7 @@ class AdminComment(models.Model): updated_at = models.DateTimeField(auto_now_add=True, blank=True) @classmethod - def add_new_comment(cls, data): - request_id = data.get('request') + def add_new_comment(cls, data, request_id): author_id = data.get('author') content = data.get('content') if not (request_id and author_id and content): @@ -259,7 +264,7 @@ class AdminComment(models.Model): if not isinstance(author, User): return create_error_response("Author Does Not Exist", status=status.HTTP_404_NOT_FOUND) - + comment = cls( request=request, author=author, @@ -274,3 +279,7 @@ class AdminComment(models.Model): return cls.objects.get(id=id) except ObjectDoesNotExist: return None + + @classmethod + def get_comments_by_request(cls, request): + return cls.objects.filter(request=request) diff --git a/geeksbot_v2/dmessages/views.py b/geeksbot_v2/dmessages/views.py index 91ea44a..f3c80ba 100644 --- a/geeksbot_v2/dmessages/views.py +++ b/geeksbot_v2/dmessages/views.py @@ -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. + +# 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) diff --git a/geeksbot_v2/entrypoint b/geeksbot_v2/entrypoint index d75e631..06d10b3 100755 --- a/geeksbot_v2/entrypoint +++ b/geeksbot_v2/entrypoint @@ -36,7 +36,7 @@ done >&2 echo 'PostgreSQL is available' python manage.py collectstatic --noinput -python manage.py makemigrations +python manage.py makemigrations --noinput python manage.py migrate /usr/bin/supervisord -c /etc/supervisor/supervisord.conf diff --git a/geeksbot_v2/guilds/api_urls.py b/geeksbot_v2/guilds/api_urls.py index ee138fa..a086d0d 100644 --- a/geeksbot_v2/guilds/api_urls.py +++ b/geeksbot_v2/guilds/api_urls.py @@ -1,9 +1,12 @@ from django.urls import path from .views import GuildsAPI, GuildDetail +from .views import RolesAPI, RoleDetailAPI -app_name = "users_api" +app_name = "guilds_api" urlpatterns = [ - path("/", view=GuildsAPI.as_view(), name="list"), - path("//", view=GuildDetail.as_view(), name='detail') + path("", view=GuildsAPI.as_view(), name="list"), + path("/", view=GuildDetail.as_view(), name='detail'), + path("/roles/", view=RolesAPI.as_view(), name="list"), + path("/roles//", view=RoleDetailAPI.as_view(), name='detail'), ] diff --git a/geeksbot_v2/guilds/migrations/0001_initial.py b/geeksbot_v2/guilds/migrations/0001_initial.py index 103c8fd..cf97b8b 100644 --- a/geeksbot_v2/guilds/migrations/0001_initial.py +++ b/geeksbot_v2/guilds/migrations/0001_initial.py @@ -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 from django.db import migrations, models @@ -28,7 +28,7 @@ class Migration(migrations.Migration): name='Role', fields=[ ('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')), ], ), diff --git a/geeksbot_v2/guilds/migrations/0002_auto_20190921_0250.py b/geeksbot_v2/guilds/migrations/0002_auto_20190921_0250.py new file mode 100644 index 0000000..49a4932 --- /dev/null +++ b/geeksbot_v2/guilds/migrations/0002_auto_20190921_0250.py @@ -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', + ), + ] diff --git a/geeksbot_v2/guilds/models.py b/geeksbot_v2/guilds/models.py index 849c003..07ed01d 100644 --- a/geeksbot_v2/guilds/models.py +++ b/geeksbot_v2/guilds/models.py @@ -15,24 +15,15 @@ from .utils import create_role_success_response class Guild(models.Model): 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) - 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)) def __str__(self): return self.id def update_guild(self, data): - if data.get('admin_chat'): - self.admin_chat = data.get('admin_chat') if 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') not in self.prefixes: self.prefixes.append(data.get('add_prefix')) @@ -55,9 +46,8 @@ class Guild(models.Model): @classmethod def create_guild(cls, data): id = data.get('id') - default_channel = data.get('default_channel') - if not (id and default_channel): - return create_error_response('id and default_channel are required', + if not id: + return create_error_response('ID is required', status=status.HTTP_400_BAD_REQUEST) if cls.get_guild_by_id(id): @@ -66,11 +56,8 @@ class Guild(models.Model): guild = cls( id=id, - default_channel=default_channel, prefixes=data.get('prefixes'), - admin_chat=data.get('admin_chat'), - new_patron_message=data.get('new_patron_message'), - new_patron_channel=data.get('new_patron_channel') + new_patron_message=data.get('new_patron_message') ) guild.save() 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) @classmethod - def add_new_role(cls, data): + def add_new_role(cls, guild_id, data): id = data.get('id') - guild_id = data.get('guild') 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", status=status.HTTP_400_BAD_REQUEST) @@ -132,5 +118,9 @@ class Role(models.Model): except ObjectDoesNotExist: return None + @classmethod + def get_guild_roles(cls, guild): + return cls.objects.filter(guild__id=guild) + def __str__(self): return f"{self.guild.id} | {self.id}" diff --git a/geeksbot_v2/guilds/serializers.py b/geeksbot_v2/guilds/serializers.py index 8dc1548..78ee353 100644 --- a/geeksbot_v2/guilds/serializers.py +++ b/geeksbot_v2/guilds/serializers.py @@ -13,4 +13,4 @@ class GuildSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer): class Meta: model = Role - fields = ["id", "guild", "type"] + fields = ["id", "guild", "role_type"] diff --git a/geeksbot_v2/guilds/views.py b/geeksbot_v2/guilds/views.py index 532896c..ca64476 100644 --- a/geeksbot_v2/guilds/views.py +++ b/geeksbot_v2/guilds/views.py @@ -5,8 +5,10 @@ from django.core.exceptions import ObjectDoesNotExist from geeksbot_v2.utils.api_utils import PaginatedAPIView from .models import Guild +from .models import Role from .utils import create_error_response from .utils import create_success_response +from .utils import create_role_success_response # Create your views here. @@ -67,3 +69,45 @@ class GuildDetail(APIView): else: return create_error_response('Guild Does Not Exist', 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) diff --git a/geeksbot_v2/patreon/migrations/0001_initial.py b/geeksbot_v2/patreon/migrations/0001_initial.py index 1c9db3e..6abcf6e 100644 --- a/geeksbot_v2/patreon/migrations/0001_initial.py +++ b/geeksbot_v2/patreon/migrations/0001_initial.py @@ -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 import django.db.models.deletion @@ -16,10 +16,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='PatreonCreator', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creator', models.CharField(max_length=50)), - ('link', models.CharField(max_length=100)), - ('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')), + ('creator', models.CharField(max_length=50, primary_key=True, serialize=False)), + ('link', models.CharField(max_length=100, unique=True)), + ('guilds', models.ManyToManyField(to='guilds.Guild')), ], ), migrations.CreateModel( @@ -30,7 +29,8 @@ class Migration(migrations.Migration): ('description', models.TextField()), ('amount', models.IntegerField(null=True)), ('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')), ], ), diff --git a/geeksbot_v2/patreon/models.py b/geeksbot_v2/patreon/models.py index fa200fd..2952305 100644 --- a/geeksbot_v2/patreon/models.py +++ b/geeksbot_v2/patreon/models.py @@ -70,7 +70,7 @@ class PatreonTier(models.Model): description = models.TextField() role = models.ForeignKey(Role, on_delete=models.CASCADE) 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): if data.get('guild'): @@ -97,17 +97,17 @@ class PatreonTier(models.Model): return create_error_response('Next Lower Tier Does Not Exist', status=status.HTTP_404_NOT_FOUND) self.next_lower_tier = tier - + self.save() return create_success_tier_response(tier, status.HTTP_202_ACCEPTED, many=False) - + @classmethod def get_tier_by_id(cls, id): try: return cls.objects.get(id=id) except ObjectDoesNotExist: return None - + @classmethod def add_new_tier(cls, data): creator_str = data.get('creator') diff --git a/geeksbot_v2/rcon/migrations/0001_initial.py b/geeksbot_v2/rcon/migrations/0001_initial.py index e58313a..ea652ae 100644 --- a/geeksbot_v2/rcon/migrations/0001_initial.py +++ b/geeksbot_v2/rcon/migrations/0001_initial.py @@ -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 import django.db.models.deletion @@ -10,9 +9,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dmessages', '0001_initial'), ('guilds', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dmessages', '0001_initial'), + ('channels', '0001_initial'), ] operations = [ @@ -25,13 +24,12 @@ class Migration(migrations.Migration): ('port', models.PositiveIntegerField()), ('password', models.CharField(max_length=50)), ('monitor_chat', models.BooleanField()), - ('monitor_chat_channel', models.CharField(blank=True, max_length=30)), - ('alerts_channel', models.CharField(blank=True, max_length=30)), - ('info_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')), ('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')), - ('settings_message', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dmessages.Message')), - ('whitelist', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('info_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')), + ('info_message', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='dmessages.Message')), + ('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')), ], ), ] diff --git a/geeksbot_v2/rcon/migrations/0002_rconserver_whitelist.py b/geeksbot_v2/rcon/migrations/0002_rconserver_whitelist.py new file mode 100644 index 0000000..9b5aa20 --- /dev/null +++ b/geeksbot_v2/rcon/migrations/0002_rconserver_whitelist.py @@ -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), + ), + ] diff --git a/geeksbot_v2/users/api_urls.py b/geeksbot_v2/users/api_urls.py index 7664afe..5007257 100644 --- a/geeksbot_v2/users/api_urls.py +++ b/geeksbot_v2/users/api_urls.py @@ -4,6 +4,6 @@ from geeksbot_v2.users.views import UsersAPI, UserDetail app_name = "users_api" urlpatterns = [ - path("/", view=UsersAPI.as_view(), name="list"), - path("//", view=UserDetail.as_view(), name="detail"), + path("", view=UsersAPI.as_view(), name="list"), + path("/", view=UserDetail.as_view(), name="detail"), ] diff --git a/geeksbot_v2/users/migrations/0001_initial.py b/geeksbot_v2/users/migrations/0001_initial.py index b2530ae..5b7996a 100644 --- a/geeksbot_v2/users/migrations/0001_initial.py +++ b/geeksbot_v2/users/migrations/0001_initial.py @@ -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 import django.contrib.auth.models @@ -25,7 +25,6 @@ class Migration(migrations.Migration): ('password', models.CharField(max_length=128, verbose_name='password')), ('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')), - ('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')), ('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')), @@ -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')), ('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')), + ('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)), ('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)), - ('discriminator', models.IntegerField(null=True)), - ('previous_discriminators', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, null=True, size=None)), + ('discriminator', models.CharField(max_length=4, null=True)), + ('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)), ('animated', models.BooleanField(blank=True, null=True)), ('avatar', models.CharField(blank=True, max_length=100, null=True)), @@ -61,9 +61,9 @@ class Migration(migrations.Migration): name='UserLog', fields=[ ('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()), - ('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)), ], ), diff --git a/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py b/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py deleted file mode 100644 index f7b4f6a..0000000 --- a/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py +++ /dev/null @@ -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'), - ), - ] diff --git a/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py b/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py deleted file mode 100644 index 9a07dad..0000000 --- a/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py +++ /dev/null @@ -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), - ), - ] diff --git a/requirements/geeksbot.txt b/requirements/geeksbot.txt index 15a787f..455219f 100644 --- a/requirements/geeksbot.txt +++ b/requirements/geeksbot.txt @@ -5,4 +5,5 @@ python-dateutil psutil pytz async_timeout -cached_property \ No newline at end of file +cached_property +redis-py \ No newline at end of file